@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c15804a0fa28d

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.
@@ -2,15 +2,17 @@ import { client, } from '@agentclientprotocol/sdk';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { unlessAborted } from './abort.js';
5
6
  import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
6
7
  import { blocksOf, firstChange } from './blocks.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
10
  import { placeFiles } from './files.js';
11
+ import { agentBase } from './home.js';
10
12
  import { instructions } from './instructions.js';
11
13
  import { holdSecretInput } from './secret-input.js';
12
14
  import { createHowWeWork } from './skills.js';
13
- const LAUNCH_BASE = '/agents/house';
15
+ import { transcribe } from './transcription.js';
14
16
  const KILLED = 'the conversation was killed';
15
17
  function causeOf(error) {
16
18
  return error instanceof Error ? error.message : String(error);
@@ -21,21 +23,15 @@ function answerMessage(request, response) {
21
23
  function logged(error) {
22
24
  process.stderr.write(`kit: ${causeOf(error)}\n`);
23
25
  }
24
- function unlessKilled(work, signal) {
25
- return new Promise((resolve, reject) => {
26
- const killed = () => reject(signal.reason);
27
- signal.addEventListener('abort', killed, { once: true });
28
- if (signal.aborted)
29
- killed();
30
- work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
31
- });
32
- }
33
26
  function stop(bridge) {
34
27
  killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
35
28
  }
36
29
  class Turn {
37
30
  id = randomUUID();
38
31
  text = '';
32
+ segment = null;
33
+ admitted = false;
34
+ notes = [];
39
35
  sent = [];
40
36
  unsentFrom = null;
41
37
  draftSequence = 0;
@@ -57,6 +53,8 @@ class Conversation {
57
53
  opening = null;
58
54
  kills = 0;
59
55
  killed = new AbortController();
56
+ interrupts = 0;
57
+ interrupted = new AbortController();
60
58
  open = 0;
61
59
  queue = Promise.resolve();
62
60
  reports = Promise.resolve();
@@ -92,6 +90,10 @@ export class Conversations {
92
90
  conversation.killed.abort(new Error(KILLED));
93
91
  this.halt(conversation);
94
92
  }
93
+ if (input.kind === 'interrupt') {
94
+ conversation.interrupts++;
95
+ conversation.interrupted.abort();
96
+ }
95
97
  const work = conversation.queue.then(() => this.carry(conversation, input));
96
98
  conversation.queue = work.catch(() => undefined);
97
99
  this.acks.set(input.input_id, work);
@@ -135,8 +137,11 @@ export class Conversations {
135
137
  return this.open(conversation, input);
136
138
  if (input.kind === 'message')
137
139
  return this.message(conversation, input);
138
- if (input.kind === 'interrupt')
140
+ if (input.kind === 'interrupt') {
141
+ if (--conversation.interrupts === 0)
142
+ conversation.interrupted = new AbortController();
139
143
  await this.interrupt(conversation, String(input.turn_id));
144
+ }
140
145
  if (input.kind === 'kill') {
141
146
  await this.kill(conversation);
142
147
  if (--conversation.kills === 0)
@@ -168,7 +173,9 @@ export class Conversations {
168
173
  const prompt = [];
169
174
  try {
170
175
  const route = await this.route(conversation, input.agent_id);
171
- const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
176
+ const files = input.files;
177
+ const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
+ const lines = await this.transcribed(conversation, files, paths);
172
179
  if (conversation.running === null) {
173
180
  const opened = await this.start(conversation, route, input.provider_session_id);
174
181
  if (input.provider_session_id === null)
@@ -181,8 +188,8 @@ export class Conversations {
181
188
  });
182
189
  }
183
190
  prompt.push({ type: 'text', text: String(input.text) });
184
- if (paths.length > 0)
185
- prompt.push({ type: 'text', text: paths.join('\n') });
191
+ if (lines.length > 0)
192
+ prompt.push({ type: 'text', text: lines.join('\n') });
186
193
  if (conversation.kills > 0)
187
194
  throw new Error(KILLED);
188
195
  }
@@ -194,6 +201,25 @@ export class Conversations {
194
201
  this.prompt(conversation, prompt);
195
202
  return ack;
196
203
  }
204
+ async transcribed(conversation, files, paths) {
205
+ const lines = [];
206
+ for (const [index, path] of paths.entries()) {
207
+ lines.push(path);
208
+ if (files[index].media_type?.startsWith('audio/') !== true)
209
+ continue;
210
+ try {
211
+ const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
+ if (text !== '')
213
+ lines.push(`Transcript: ${text}`);
214
+ }
215
+ catch (error) {
216
+ if (conversation.kills > 0)
217
+ throw error;
218
+ logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
219
+ }
220
+ }
221
+ return lines;
222
+ }
197
223
  async interrupt(conversation, turnId) {
198
224
  if (conversation.running === null || conversation.turn?.id !== turnId)
199
225
  return;
@@ -232,13 +258,13 @@ export class Conversations {
232
258
  }
233
259
  async route(conversation, agentId) {
234
260
  const signal = conversation.killed.signal;
235
- await unlessKilled(this.kit.agents.read(), signal);
261
+ await unlessAborted(this.kit.agents.read(), signal);
236
262
  if (this.kit.agents.route(agentId) === undefined)
237
- await unlessKilled(this.kit.agents.refresh(), signal);
263
+ await unlessAborted(this.kit.agents.refresh(), signal);
238
264
  const route = this.kit.agents.route(agentId);
239
265
  if (route === undefined)
240
266
  throw new Error(`this Environment hosts no Agent ${agentId}`);
241
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
267
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
242
268
  await mkdir(route.working_directory, { recursive: true });
243
269
  }
244
270
  return route;
@@ -250,7 +276,7 @@ export class Conversations {
250
276
  .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
251
277
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
252
278
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
253
- await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
279
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
254
280
  const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
255
281
  if (conversation.kills > 0) {
256
282
  bridge.close();
@@ -258,8 +284,8 @@ export class Conversations {
258
284
  }
259
285
  conversation.opening = bridge;
260
286
  try {
261
- await unlessKilled(createHowWeWork(bridge), conversation.killed.signal);
262
- const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
287
+ await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
263
289
  void adapter.exited.then(() => bridge.close());
264
290
  const agent = adapter.connection.agent;
265
291
  const cwd = route.working_directory;
@@ -272,6 +298,7 @@ export class Conversations {
272
298
  sessionId: opened.sessionId,
273
299
  killed: false,
274
300
  queues: CLIS[route.kind].queues,
301
+ phase: CLIS[route.kind].phase,
275
302
  };
276
303
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
277
304
  conversation.running = running;
@@ -359,6 +386,9 @@ export class Conversations {
359
386
  return turn;
360
387
  }
361
388
  admitted(conversation, turn) {
389
+ turn.admitted = true;
390
+ for (const note of turn.notes.splice(0))
391
+ this.kit.send(note);
362
392
  if (turn.text !== '') {
363
393
  turn.unsentFrom = 0;
364
394
  this.draft(conversation, turn);
@@ -372,6 +402,17 @@ export class Conversations {
372
402
  this.kit.changed();
373
403
  return turn;
374
404
  }
405
+ finish(conversation, turn) {
406
+ if (turn.ended)
407
+ return;
408
+ const segment = turn.segment;
409
+ turn.segment = null;
410
+ if (segment?.phase === 'note')
411
+ this.note(conversation, turn, segment.text);
412
+ else if (segment !== null)
413
+ this.answer(conversation, turn, segment.text);
414
+ this.end(conversation, turn, { text: turn.text });
415
+ }
375
416
  end(conversation, turn, outcome) {
376
417
  if (turn.ended)
377
418
  return;
@@ -429,7 +470,7 @@ export class Conversations {
429
470
  this.end(conversation, turn, { failed });
430
471
  }
431
472
  else if (turn.prompt === sent) {
432
- this.end(conversation, turn, { text: turn.text });
473
+ this.finish(conversation, turn);
433
474
  }
434
475
  }
435
476
  started(conversation) {
@@ -446,7 +487,7 @@ export class Conversations {
446
487
  if (turn === null)
447
488
  return;
448
489
  if (turn.prompt === null) {
449
- this.end(conversation, turn, { text: turn.text });
490
+ this.finish(conversation, turn);
450
491
  }
451
492
  else {
452
493
  conversation.turn = null;
@@ -500,17 +541,21 @@ export class Conversations {
500
541
  update(conversation, notification) {
501
542
  const update = notification.update;
502
543
  if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
503
- const turn = this.started(conversation);
504
- turn.text += update.content.text;
505
- this.draft(conversation, turn);
544
+ this.chunk(conversation, this.started(conversation), update, update.content.text);
506
545
  }
507
546
  else if (update.sessionUpdate === 'plan') {
508
547
  const turn = this.started(conversation);
548
+ this.close(conversation, turn);
509
549
  turn.entries = update.entries;
510
550
  this.plan(conversation, turn);
511
551
  }
512
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
513
- this.started(conversation);
552
+ else if (update.sessionUpdate === 'tool_call') {
553
+ this.close(conversation, this.started(conversation));
554
+ }
555
+ else if (update.sessionUpdate === 'agent_thought_chunk') {
556
+ const turn = this.started(conversation);
557
+ if (turn.segment?.messageId !== (update.messageId ?? null))
558
+ this.close(conversation, turn);
514
559
  }
515
560
  else if (update.sessionUpdate === 'available_commands_update') {
516
561
  this.commands(conversation, update.availableCommands);
@@ -526,6 +571,38 @@ export class Conversations {
526
571
  conversation.jobs.delete(job.id);
527
572
  this.kit.changed();
528
573
  }
574
+ chunk(conversation, turn, chunk, text) {
575
+ const phase = conversation.running?.phase(chunk) ?? null;
576
+ const messageId = chunk.messageId ?? null;
577
+ if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
578
+ this.close(conversation, turn);
579
+ if (phase === 'answer') {
580
+ this.answer(conversation, turn, text);
581
+ return;
582
+ }
583
+ turn.segment ??= { messageId, phase, text: '' };
584
+ turn.segment.text += text;
585
+ }
586
+ close(conversation, turn) {
587
+ const segment = turn.segment;
588
+ if (segment === null)
589
+ return;
590
+ turn.segment = null;
591
+ this.note(conversation, turn, segment.text);
592
+ }
593
+ note(conversation, turn, text) {
594
+ if (text.trim() === '')
595
+ return;
596
+ const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
+ if (turn.admitted)
598
+ this.kit.send(note);
599
+ else
600
+ turn.notes.push(note);
601
+ }
602
+ answer(conversation, turn, text) {
603
+ turn.text += text;
604
+ this.draft(conversation, turn);
605
+ }
529
606
  draft(conversation, turn) {
530
607
  const blocks = blocksOf(turn.text);
531
608
  const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
package/dist/copies.js CHANGED
@@ -3,12 +3,13 @@ import { 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
5
  import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
6
+ import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
+ import { refusalOf } from './refusals.js';
9
10
  import { changed } from './translate.js';
11
+ import { relayed } from './relay.js';
10
12
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
13
  const PROVENANCE = '_provenance';
13
14
  const REPLICA_RETRY_MS = 30_000;
14
15
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
@@ -18,13 +19,16 @@ function causeOf(error) {
18
19
  function logged(error) {
19
20
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
21
  }
21
- export function refusalCode(text) {
22
- try {
23
- return String(JSON.parse(text).error.code);
24
- }
25
- catch {
26
- return text.split(':')[0].trim();
27
- }
22
+ const REBASED = new Set([
23
+ 'field_base_stale',
24
+ 'section_base_stale',
25
+ 'preamble_base_stale',
26
+ 'base_revision_stale',
27
+ 'path_exists',
28
+ 'path_absent',
29
+ ]);
30
+ function rebaseFixes(refusal) {
31
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
28
32
  }
29
33
  function short(commit) {
30
34
  return commit.slice(0, 12);
@@ -37,14 +41,11 @@ function sidecar(path) {
37
41
  segments.splice(-1, 0, PROVENANCE);
38
42
  return segments.join('/');
39
43
  }
40
- function rootOf(copy) {
41
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
44
+ function rootOf(handle) {
45
+ return `/rooms/${handle}`;
42
46
  }
43
47
  function relative(path) {
44
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
45
- }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
48
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
49
  }
49
50
  function declaredWritable(path, copy) {
50
51
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -147,7 +148,7 @@ function movedPaths(moves, before, after) {
147
148
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
149
  .map(({ from, to }) => [from, to]));
149
150
  }
150
- export class WorkingCopies {
151
+ export class LocalCopies {
151
152
  owner;
152
153
  house;
153
154
  copies = new Map();
@@ -166,15 +167,17 @@ export class WorkingCopies {
166
167
  catch (error) {
167
168
  if (!(error instanceof HouseRefusal) || error.status >= 500)
168
169
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
170
+ return {
171
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
172
+ };
170
173
  }
171
174
  },
172
175
  };
173
176
  }
174
177
  async load() {
175
- const home = join(kitHome(), 'working-copies');
178
+ const home = join(kitHome(), 'local-copies');
176
179
  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')]) {
180
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
178
181
  const text = await readFile(path, 'utf8').catch(() => null);
179
182
  if (text === null)
180
183
  continue;
@@ -186,7 +189,7 @@ export class WorkingCopies {
186
189
  await this.selecting;
187
190
  return [...this.copies.values()]
188
191
  .filter((copy) => copy.active)
189
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
192
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
190
193
  .sort();
191
194
  }
192
195
  select() {
@@ -195,13 +198,8 @@ export class WorkingCopies {
195
198
  return selected;
196
199
  }
197
200
  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
- ];
201
+ const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
202
+ const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
205
203
  const keys = new Set(selected.map((scope) => scope.key));
206
204
  const work = [];
207
205
  for (const key of this.copies.keys()) {
@@ -253,11 +251,11 @@ export class WorkingCopies {
253
251
  async push(cwd, selector, caller) {
254
252
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
253
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
254
+ return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
257
255
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
256
  const key = await this.keyAt(dirname(common));
259
257
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
258
+ return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
261
259
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
260
  }
263
261
  async keyAt(repository) {
@@ -285,7 +283,7 @@ export class WorkingCopies {
285
283
  }).catch(logged);
286
284
  }
287
285
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
286
+ const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
289
287
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
288
  const temporary = `${path}.${process.pid}`;
291
289
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -293,12 +291,12 @@ export class WorkingCopies {
293
291
  this.copies.set(copy.key, copy);
294
292
  return copy;
295
293
  }
296
- async read(selected) {
297
- const prefixes = prefixesOf(selected);
294
+ async read(handle) {
295
+ const prefixes = [rootOf(handle)];
298
296
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
297
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
298
  if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
299
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
302
300
  const [view] = bundle.scopes;
303
301
  if (view === undefined)
304
302
  return null;
@@ -341,13 +339,13 @@ export class WorkingCopies {
341
339
  return;
342
340
  }
343
341
  const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
342
+ prefixes: [rootOf(copy.room_handle)],
345
343
  });
346
344
  const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
345
  if (authority === undefined)
348
346
  return;
349
347
  if (authority.managed_replication !== 'admitted') {
350
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
348
+ logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
351
349
  return;
352
350
  }
353
351
  if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
@@ -358,12 +356,12 @@ export class WorkingCopies {
358
356
  await this.save(await this.caughtUp(copy));
359
357
  }
360
358
  async materialize(scope) {
361
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
359
+ const read = await this.read(scope.handle);
362
360
  if (read === null) {
363
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
361
+ logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
364
362
  return;
365
363
  }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
364
+ const repository = join(localCopies(), 'rooms', read.anchor);
367
365
  if (await exists(repository)) {
368
366
  logged(`${repository} already exists and House Kit leaves it as it is`);
369
367
  return;
@@ -375,7 +373,7 @@ export class WorkingCopies {
375
373
  try {
376
374
  await git(staging, ['init', '-q', '-b', BRANCH]);
377
375
  house = await importCommits(staging, [
378
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
376
+ { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
379
377
  ]);
380
378
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
381
379
  await git(staging, ['reset', '-q', '--hard']);
@@ -389,7 +387,6 @@ export class WorkingCopies {
389
387
  }
390
388
  await this.save({
391
389
  key: scope.key,
392
- private: scope.private,
393
390
  room_ref: read.anchor,
394
391
  room_handle: scope.handle,
395
392
  repository,
@@ -405,11 +402,11 @@ export class WorkingCopies {
405
402
  });
406
403
  }
407
404
  async received(copy) {
408
- const read = await this.read(copy);
405
+ const read = await this.read(copy.room_handle);
409
406
  if (read === null)
410
407
  return copy;
411
408
  const house = await importCommits(copy.repository, [
412
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
409
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
413
410
  ]);
414
411
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
415
412
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -432,12 +429,12 @@ export class WorkingCopies {
432
429
  let answer;
433
430
  try {
434
431
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
432
+ prefixes: [rootOf(copy.room_handle)],
436
433
  positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
437
434
  });
438
435
  }
439
436
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
437
+ const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
441
438
  if (code === 'position_expired')
442
439
  return this.received(copy);
443
440
  if (code !== 'replica_behind')
@@ -482,7 +479,7 @@ export class WorkingCopies {
482
479
  let { house, manifests } = copy;
483
480
  if (files.size > 0) {
484
481
  house = await importCommits(copy.repository, [
485
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
482
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
486
483
  ]);
487
484
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
488
485
  }
@@ -512,8 +509,9 @@ export class WorkingCopies {
512
509
  }
513
510
  copy = await this.save(without(copy));
514
511
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
512
+ if (!copy.active) {
513
+ return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
514
+ }
517
515
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
516
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
517
  if (base === undefined) {
@@ -546,7 +544,7 @@ export class WorkingCopies {
546
544
  return this.settle(submitted, caller);
547
545
  }
548
546
  async derived(copy, base, commit) {
549
- const root = rootOf(copy);
547
+ const root = rootOf(copy.room_handle);
550
548
  const before = await tree(copy.repository, base);
551
549
  const after = await tree(copy.repository, commit);
552
550
  const moved = movedPaths(copy.moves, before, after);
@@ -582,9 +580,8 @@ export class WorkingCopies {
582
580
  async settle(start, caller, restaged = false) {
583
581
  let copy = start;
584
582
  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
- }
583
+ if (push.caller !== caller.key)
584
+ return this.recovered(copy);
588
585
  let outcome;
589
586
  try {
590
587
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +589,10 @@ export class WorkingCopies {
592
589
  });
593
590
  }
594
591
  catch (error) {
592
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
593
  return {
596
594
  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.`,
595
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
596
  };
599
597
  }
600
598
  if ('refused' in outcome) {
@@ -603,31 +601,37 @@ export class WorkingCopies {
603
601
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
602
  }
605
603
  if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
604
+ return this.recovered(copy);
605
+ const rebased = rebaseFixes(outcome.refused);
607
606
  let settled = await this.save(without(copy));
608
607
  try {
609
608
  settled = await this.save(await this.caughtUp(settled));
610
609
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
610
+ catch {
611
+ return {
612
+ refused: true,
613
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
614
+ };
613
615
  }
614
616
  return {
615
617
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
618
+ text: rebased
619
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
620
+ : outcome.refused.text,
617
621
  };
618
622
  }
619
623
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
624
  }
621
- async recovered(copy, reason) {
625
+ async recovered(copy) {
622
626
  const push = copy.push;
623
627
  const current = await this.save(await this.caughtUp(copy));
624
628
  const house = await tree(copy.repository, current.house);
625
- const root = rootOf(current);
629
+ const root = rootOf(current.room_handle);
626
630
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
627
631
  await this.save(without(current));
628
632
  return {
629
633
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
634
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
635
  };
632
636
  }
633
637
  const written = [];
@@ -652,10 +656,10 @@ export class WorkingCopies {
652
656
  await this.save(without(copy));
653
657
  return { refused: false, text: integrated.text };
654
658
  }
655
- catch (error) {
659
+ catch {
656
660
  return {
657
661
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
662
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
663
  };
660
664
  }
661
665
  }
@@ -678,7 +682,7 @@ export class WorkingCopies {
678
682
  written = [
679
683
  ...written,
680
684
  {
681
- path: `${rootOf(copy)}/${path}`,
685
+ path: `${rootOf(copy.room_handle)}/${path}`,
682
686
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
683
687
  content: entry === undefined ? null : await blobText(copy.repository, entry, path),
684
688
  },
@@ -698,7 +702,7 @@ export class WorkingCopies {
698
702
  }
699
703
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
700
704
  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`;
705
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
702
706
  const house = await importCommits(copy.repository, [
703
707
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
704
708
  {
package/dist/files.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { relayed } from './relay.js';
3
4
  export async function placeFiles(house, directory, files, signal) {
4
5
  const paths = [];
5
6
  for (const file of files) {
6
7
  const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
7
- const answer = await fetch(original.download.url, { signal });
8
+ const answer = await fetch(relayed(original.download.url), { signal });
8
9
  if (!answer.ok)
9
10
  throw new Error(`${file.name} did not download: ${answer.status}`);
10
11
  const folder = join(directory, '.house', 'files', original.message, file.version);
package/dist/git-main.js CHANGED
@@ -4,7 +4,7 @@ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
4
4
  import { request } from 'node:http';
5
5
  import { constants as system } from 'node:os';
6
6
  import { delimiter, dirname, join } from 'node:path';
7
- import { WORKING_COPIES } from './git.js';
7
+ import { localCopies } from './git.js';
8
8
  import { kitHome } from './home.js';
9
9
  const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
10
10
  const READS = new Set([
@@ -125,7 +125,7 @@ async function observed(git, args) {
125
125
  return delegated(git, args);
126
126
  const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
127
127
  const [common, worktree] = located?.split('\n') ?? [];
128
- if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${WORKING_COPIES}/`)) {
128
+ if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${localCopies()}/`)) {
129
129
  return delegated(git, args);
130
130
  }
131
131
  const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];