@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3e86aacccc80

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.
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
- import { killMarked, processes, startAdapter } from './acp.js';
6
+ import { startAdapter } from './acp.js';
7
7
  import { openBridge } from './bridge.js';
8
8
  import { CLIS } from './clis.js';
9
9
  import { readerFor } from './events.js';
@@ -11,12 +11,12 @@ import { placeFiles, uploadBytes } from './files.js';
11
11
  import { agentBase } from './home.js';
12
12
  import { instructions } from './instructions.js';
13
13
  import { Parts } from './parts.js';
14
+ import { killMarked, Scope } from './scope.js';
14
15
  import { holdSecretInput } from './secret-input.js';
15
16
  import { createHowWeWork } from './skills.js';
16
17
  import { transcribe } from './transcription.js';
18
+ import { workFor } from './work.js';
17
19
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
20
20
  function causeOf(error) {
21
21
  return error instanceof Error ? error.message : String(error);
22
22
  }
@@ -29,9 +29,6 @@ function logged(error) {
29
29
  function marker(bridge) {
30
30
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
31
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
34
- }
35
32
  class Turn {
36
33
  id = randomUUID();
37
34
  parts;
@@ -53,7 +50,7 @@ class Conversation {
53
50
  turn = null;
54
51
  commands = null;
55
52
  context = null;
56
- busy = new Set();
53
+ leftovers = new Set();
57
54
  waiting = [];
58
55
  sending = null;
59
56
  subagents = new Map();
@@ -86,9 +83,10 @@ export class Conversations {
86
83
  carrying = 0;
87
84
  reporting = 0;
88
85
  watched = false;
89
- recheck = null;
90
86
  constructor(kit) {
91
87
  this.kit = kit;
88
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
+ setInterval(() => this.look(), 2_000);
92
90
  }
93
91
  input(input) {
94
92
  this.carrying++;
@@ -126,22 +124,27 @@ export class Conversations {
126
124
  for (const conversation of this.conversations.values()) {
127
125
  const turn = conversation.turn;
128
126
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
127
+ if (conversation.open > (asking ? 1 : 0))
130
128
  return false;
131
129
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
130
+ const conversations = [...this.conversations.values()];
131
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
132
  return false;
133
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
+ }
135
+ quiet() {
136
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
137
+ }
138
+ look() {
139
+ for (const conversation of this.conversations.values()) {
140
+ for (const scope of conversation.leftovers) {
141
+ if (!scope.empty())
142
+ continue;
143
+ scope.close();
144
+ conversation.leftovers.delete(scope);
145
+ }
143
146
  }
144
- return true;
147
+ this.kit.changed();
145
148
  }
146
149
  opened() {
147
150
  this.watched = false;
@@ -295,11 +298,13 @@ export class Conversations {
295
298
  }
296
299
  halt(conversation) {
297
300
  if (conversation.opening !== null)
298
- stop(conversation.opening);
301
+ killMarked(marker(conversation.opening));
302
+ for (const scope of conversation.leftovers)
303
+ scope.kill();
299
304
  if (conversation.running === null)
300
305
  return;
301
306
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
307
+ conversation.running.scope.kill();
303
308
  }
304
309
  async kill(conversation) {
305
310
  const running = conversation.running;
@@ -350,22 +355,30 @@ export class Conversations {
350
355
  throw new Error(KILLED);
351
356
  }
352
357
  conversation.opening = bridge;
358
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
353
360
  try {
354
361
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
362
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
364
+ work.read(message);
365
+ this.read(conversation, reader, message);
366
+ }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
358
367
  void adapter.exited.then(() => bridge.close());
359
368
  const agent = adapter.connection.agent;
360
369
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
370
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
362
371
  const opened = sessionId === null
363
372
  ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
373
  : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
365
374
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
375
+ if (sessionId === null && CLIS[route.kind].renames) {
376
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
+ await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
378
+ }
379
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
367
380
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
381
+ void work.settled().then(() => scope.record());
369
382
  conversation.running = running;
370
383
  conversation.session = running.sessionId;
371
384
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
@@ -376,7 +389,9 @@ export class Conversations {
376
389
  return running.sessionId;
377
390
  }
378
391
  catch (error) {
379
- stop(bridge);
392
+ scope.kill();
393
+ work.close();
394
+ conversation.leftovers.add(scope);
380
395
  bridge.close();
381
396
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
397
  }
@@ -409,17 +424,18 @@ export class Conversations {
409
424
  const running = conversation.running;
410
425
  if (running?.adapter !== adapter)
411
426
  return;
412
- this.detach(conversation);
427
+ this.detach(conversation, running);
413
428
  if (conversation.turn !== null) {
414
429
  this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
415
430
  }
416
431
  this.next(conversation);
417
432
  this.kit.changed();
418
433
  }
419
- detach(conversation) {
434
+ detach(conversation, running) {
435
+ running.work.close();
436
+ conversation.leftovers.add(running.scope);
420
437
  conversation.running = null;
421
438
  conversation.commands = null;
422
- conversation.busy.clear();
423
439
  for (const [id, question] of this.questions) {
424
440
  if (question.conversation === conversation)
425
441
  this.questions.delete(id);
@@ -471,8 +487,6 @@ export class Conversations {
471
487
  turn.parts.measure(conversation.context);
472
488
  for (const part of conversation.held.splice(0))
473
489
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
490
  this.kit.changed();
477
491
  return turn;
478
492
  }
@@ -488,7 +502,7 @@ export class Conversations {
488
502
  if (conversation.turn === turn)
489
503
  conversation.turn = null;
490
504
  const body = Promise.allSettled(turn.uploads).then(() => ({
491
- parts: turn.parts.parts,
505
+ parts: turn.parts.saved(),
492
506
  context: turn.parts.context,
493
507
  ...failed,
494
508
  }));
@@ -506,8 +520,8 @@ export class Conversations {
506
520
  }
507
521
  else {
508
522
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
523
+ running.scope.kill();
524
+ this.detach(conversation, running);
511
525
  }
512
526
  this.end(conversation, turn, { failed: causeOf(error) });
513
527
  }
@@ -592,20 +606,6 @@ export class Conversations {
592
606
  case 'options':
593
607
  this.options(conversation, event.options);
594
608
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
609
  case 'context':
610
610
  conversation.context = { used: event.used, window: event.window };
611
611
  conversation.turn?.parts.measure(conversation.context);
@@ -647,7 +647,7 @@ export class Conversations {
647
647
  turn.parts.plan(event.entries);
648
648
  return;
649
649
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
650
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
651
  this.kit.changed();
652
652
  return;
653
653
  }
@@ -667,7 +667,7 @@ export class Conversations {
667
667
  else
668
668
  conversation.turn.parts.start(part);
669
669
  }
670
- async image(conversation, turn, data, mediaType) {
670
+ async image(conversation, turn, reserved, data, mediaType) {
671
671
  const bytes = Buffer.from(data, 'base64');
672
672
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
673
673
  try {
@@ -680,10 +680,10 @@ export class Conversations {
680
680
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
681
  if (saved.save.status !== 'saved')
682
682
  throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
683
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
684
684
  }
685
685
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
686
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
687
687
  }
688
688
  }
689
689
  }
package/dist/copies.js CHANGED
@@ -32,6 +32,22 @@ function rebaseFixes(refusal) {
32
32
  function short(commit) {
33
33
  return commit.slice(0, 12);
34
34
  }
35
+ function moved(before, after) {
36
+ if (before.house !== after.house)
37
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
38
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
39
+ }
40
+ function expiredAuthority(error) {
41
+ if (!(error instanceof HouseRefusal))
42
+ return undefined;
43
+ try {
44
+ const { error: refused } = JSON.parse(error.text);
45
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
35
51
  function covers(path, prefix) {
36
52
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
53
  }
@@ -152,8 +168,7 @@ export class LocalCopies {
152
168
  house;
153
169
  copies = new Map();
154
170
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
171
+ answering = Promise.resolve();
157
172
  constructor(house) {
158
173
  this.house = house;
159
174
  this.owner = {
@@ -185,42 +200,68 @@ export class LocalCopies {
185
200
  }
186
201
  }
187
202
  async mapping() {
188
- await this.selecting;
203
+ await this.answering;
189
204
  return [...this.copies.values()]
190
205
  .filter((copy) => copy.active)
191
206
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
207
  .sort();
193
208
  }
194
- select() {
195
- const selected = this.selecting.then(() => this.reselect());
196
- this.selecting = selected.catch(() => undefined);
197
- return selected;
198
- }
199
- async reselect() {
200
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
201
- const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
202
- const keys = new Set(selected.map((scope) => scope.key));
203
- const work = [];
204
- for (const key of this.copies.keys()) {
205
- if (keys.has(key))
206
- continue;
207
- work.push(this.exclusive(key, async () => {
208
- const copy = this.copies.get(key);
209
- if (copy.active)
210
- await this.save({ ...copy, active: false });
211
- }));
212
- }
213
- for (const scope of selected)
214
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
215
- await Promise.all(work);
209
+ async connected() {
210
+ const copied = await this.answered(() => this.fetched(false));
211
+ if (copied.refused)
212
+ logged(copied.text);
216
213
  }
217
- entries(frame) {
218
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
219
- if (copy === undefined)
220
- return;
221
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
222
- return;
223
- this.deliver(copy.key);
214
+ fetch() {
215
+ return this.answered(() => this.fetched(true));
216
+ }
217
+ answered(work) {
218
+ const answered = this.answering.then(work);
219
+ this.answering = answered.catch(() => undefined);
220
+ return answered;
221
+ }
222
+ async fetched(refreshing) {
223
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
224
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
226
+ const said = [];
227
+ let refused = false;
228
+ for (const copy of this.copies.values()) {
229
+ if (synced.has(copy.key) || !copy.active)
230
+ continue;
231
+ await this.save({ ...copy, active: false });
232
+ said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
233
+ }
234
+ const current = [];
235
+ for (const [key, handle] of synced) {
236
+ try {
237
+ const held = this.copies.get(key);
238
+ if (held === undefined || !(await exists(held.repository))) {
239
+ said.push(await this.materialize(key, handle));
240
+ continue;
241
+ }
242
+ const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
243
+ if (copy.active)
244
+ current.push(copy);
245
+ else
246
+ said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
247
+ }
248
+ catch (error) {
249
+ refused = true;
250
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
251
+ }
252
+ }
253
+ if (refreshing && current.length > 0) {
254
+ try {
255
+ const refreshed = await this.refreshed(current);
256
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
257
+ }
258
+ catch (error) {
259
+ refused = true;
260
+ said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
261
+ }
262
+ }
263
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
264
+ });
224
265
  }
225
266
  async observed(report) {
226
267
  const key = await this.keyAt(dirname(report.common));
@@ -270,16 +311,9 @@ export class LocalCopies {
270
311
  this.queues.set(key, queued.catch(() => undefined));
271
312
  return queued;
272
313
  }
273
- deliver(key) {
274
- if (this.delivering.has(key))
275
- return;
276
- this.delivering.add(key);
277
- void this.exclusive(key, async () => {
278
- this.delivering.delete(key);
279
- const copy = this.copies.get(key);
280
- if (copy.active)
281
- await this.save(await this.caughtUp(copy));
282
- }).catch(logged);
314
+ holding(keys, work) {
315
+ const [key, ...rest] = keys;
316
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
317
  }
284
318
  async save(copy) {
285
319
  const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
@@ -325,46 +359,13 @@ export class LocalCopies {
325
359
  }
326
360
  return read;
327
361
  }
328
- async apply(scope) {
329
- let copy = this.copies.get(scope.key);
330
- if (copy === undefined || !(await exists(copy.repository))) {
331
- await this.materialize(scope);
332
- return;
333
- }
334
- if (copy.room_handle !== scope.handle)
335
- copy = await this.save({ ...copy, room_handle: scope.handle });
336
- if (!copy.active) {
337
- await this.save(await this.received({ ...copy, active: true }));
338
- return;
339
- }
340
- const discovery = await this.house.post('/kit/door/discover', {
341
- prefixes: [rootOf(copy.room_handle)],
342
- });
343
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
344
- if (authority === undefined)
345
- return;
346
- if (authority.managed_replication !== 'admitted') {
347
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
348
- return;
349
- }
350
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
351
- JSON.stringify([copy.write, copy.protected])) {
352
- await this.save(await this.received(copy));
353
- return;
354
- }
355
- await this.save(await this.caughtUp(copy));
356
- }
357
- async materialize(scope) {
358
- const read = await this.read(scope.handle);
359
- if (read === null) {
360
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
361
- return;
362
- }
362
+ async materialize(key, handle) {
363
+ const read = await this.read(handle);
364
+ if (read === null)
365
+ throw new Error('House serves nothing of it to copy');
363
366
  const repository = join(localCopies(), 'rooms', read.anchor);
364
- if (await exists(repository)) {
365
- logged(`${repository} already exists and House Kit leaves it as it is`);
366
- return;
367
- }
367
+ if (await exists(repository))
368
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
369
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
370
  await mkdir(staging, { recursive: true });
370
371
  let house;
@@ -372,7 +373,7 @@ export class LocalCopies {
372
373
  try {
373
374
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
375
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
376
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
377
  ]);
377
378
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
379
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,9 +386,9 @@ export class LocalCopies {
385
386
  throw error;
386
387
  }
387
388
  await this.save({
388
- key: scope.key,
389
+ key,
389
390
  room_ref: read.anchor,
390
- room_handle: scope.handle,
391
+ room_handle: handle,
391
392
  repository,
392
393
  active: true,
393
394
  house,
@@ -399,6 +400,7 @@ export class LocalCopies {
399
400
  manifests: { [house]: { parent: null, files: read.revisions } },
400
401
  moves: [],
401
402
  });
403
+ return `${rootOf(handle)}: copied to ${repository}`;
402
404
  }
403
405
  async received(copy) {
404
406
  const read = await this.read(copy.room_handle);
@@ -422,29 +424,52 @@ export class LocalCopies {
422
424
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
425
  return next;
424
426
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
427
+ async refreshed(copies) {
428
+ const discovery = await this.house.post('/kit/door/discover', {
429
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
430
+ });
431
+ const regranted = new Map();
432
+ for (const copy of copies) {
433
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
434
+ if (authority !== undefined &&
435
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
436
+ regranted.set(copy.key, await this.save(await this.received(copy)));
437
+ }
438
+ }
439
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
440
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
441
+ }
442
+ async caughtUp(copies, whole) {
443
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
444
+ let asking = [...copies];
445
+ while (asking.length > 0) {
428
446
  let answer;
429
447
  try {
430
448
  answer = await this.house.post('/kit/door/enumerate', {
431
- prefixes: [rootOf(copy.room_handle)],
432
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
449
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
450
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
451
  });
434
452
  }
435
453
  catch (error) {
436
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
437
- if (code === 'position_expired')
438
- return this.received(copy);
439
- if (code !== 'replica_behind')
454
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
455
+ if (expired === undefined)
440
456
  throw error;
441
- return copy;
457
+ caught.set(expired.key, await this.save(await this.received(expired)));
458
+ asking = asking.filter((copy) => copy !== expired);
459
+ continue;
460
+ }
461
+ const advanced = [];
462
+ for (const copy of asking) {
463
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
464
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
465
+ continue;
466
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
467
+ caught.set(copy.key, next);
468
+ advanced.push(next);
442
469
  }
443
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
444
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
445
- return copy;
446
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
470
+ asking = whole ? advanced : [];
447
471
  }
472
+ return copies.map((copy) => caught.get(copy.key));
448
473
  }
449
474
  async incorporated(copy, changes, position, logEpoch) {
450
475
  const houseTree = await tree(copy.repository, copy.house);
@@ -598,12 +623,12 @@ export class LocalCopies {
598
623
  const { upload, ...unstaged } = copy.push;
599
624
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
625
  }
601
- if (outcome.refused.code === 'operation_expired')
626
+ if (outcome.refused.code === 'expired')
602
627
  return this.recovered(copy);
603
628
  const rebased = rebaseFixes(outcome.refused);
604
629
  let settled = await this.save(without(copy));
605
630
  try {
606
- settled = await this.save(await this.caughtUp(settled));
631
+ settled = (await this.caughtUp([settled], true))[0];
607
632
  }
608
633
  catch {
609
634
  return {
@@ -622,7 +647,7 @@ export class LocalCopies {
622
647
  }
623
648
  async recovered(copy) {
624
649
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
650
+ const current = (await this.caughtUp([copy], true))[0];
626
651
  const house = await tree(copy.repository, current.house);
627
652
  const root = rootOf(current.room_handle);
628
653
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -666,7 +691,7 @@ export class LocalCopies {
666
691
  const push = copy.push;
667
692
  let written = push.written;
668
693
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
694
+ copy = (await this.caughtUp([copy], true))[0];
670
695
  const house = await tree(copy.repository, copy.house);
671
696
  const sent = await tree(copy.repository, push.house);
672
697
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
package/dist/end.js CHANGED
@@ -1,8 +1,18 @@
1
- import { killDescendants } from './acp.js';
1
+ import { forgetRejected } from './home.js';
2
+ import { killDescendants } from './scope.js';
2
3
  export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
4
+ function exit(reason, status) {
5
5
  process.stderr.write(`kit: ${reason}\n`);
6
6
  killDescendants();
7
- process.exit(0);
7
+ process.exit(status);
8
+ }
9
+ export function end(reason) {
10
+ exit(reason, 0);
11
+ }
12
+ export function restart(reason) {
13
+ exit(reason, 1);
14
+ }
15
+ export async function signInAgain(rejected) {
16
+ await forgetRejected(rejected);
17
+ restart('sign in again');
8
18
  }
@@ -1,4 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { writeEnrolment } from './home.js';
3
3
  const [house, environment] = process.argv.slice(2);
4
- await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
4
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
5
+ await writeEnrolment({
6
+ house: house,
7
+ ...(relay === undefined ? {} : { relay }),
8
+ environment: environment,
9
+ credential: readFileSync(0, 'utf8').trim(),
10
+ });