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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/copies.js CHANGED
@@ -2,15 +2,15 @@ import { parse } from '@agentshouse/mdmodel';
2
2
  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
- 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 { 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
- const REPLICA_RETRY_MS = 30_000;
14
14
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
@@ -18,13 +18,16 @@ function causeOf(error) {
18
18
  function logged(error) {
19
19
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
20
  }
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
- }
21
+ const REBASED = new Set([
22
+ 'field_base_stale',
23
+ 'section_base_stale',
24
+ 'preamble_base_stale',
25
+ 'base_revision_stale',
26
+ 'path_exists',
27
+ 'path_absent',
28
+ ]);
29
+ function rebaseFixes(refusal) {
30
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
28
31
  }
29
32
  function short(commit) {
30
33
  return commit.slice(0, 12);
@@ -37,14 +40,11 @@ function sidecar(path) {
37
40
  segments.splice(-1, 0, PROVENANCE);
38
41
  return segments.join('/');
39
42
  }
40
- function rootOf(copy) {
41
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
43
+ function rootOf(handle) {
44
+ return `/rooms/${handle}`;
42
45
  }
43
46
  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}`);
47
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
48
  }
49
49
  function declaredWritable(path, copy) {
50
50
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -147,7 +147,7 @@ function movedPaths(moves, before, after) {
147
147
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
148
  .map(({ from, to }) => [from, to]));
149
149
  }
150
- export class WorkingCopies {
150
+ export class LocalCopies {
151
151
  owner;
152
152
  house;
153
153
  copies = new Map();
@@ -160,21 +160,23 @@ export class WorkingCopies {
160
160
  key: 'owner',
161
161
  submit: async (operation, changes) => {
162
162
  try {
163
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
163
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
164
164
  return { accepted: outcome.contents };
165
165
  }
166
166
  catch (error) {
167
- if (!(error instanceof HouseRefusal) || error.status >= 500)
167
+ if (!finalRefusal(error))
168
168
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
169
+ return {
170
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
171
+ };
170
172
  }
171
173
  },
172
174
  };
173
175
  }
174
176
  async load() {
175
- const home = join(kitHome(), 'working-copies');
177
+ const home = join(kitHome(), 'local-copies');
176
178
  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')]) {
179
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
178
180
  const text = await readFile(path, 'utf8').catch(() => null);
179
181
  if (text === null)
180
182
  continue;
@@ -186,7 +188,7 @@ export class WorkingCopies {
186
188
  await this.selecting;
187
189
  return [...this.copies.values()]
188
190
  .filter((copy) => copy.active)
189
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
191
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
190
192
  .sort();
191
193
  }
192
194
  select() {
@@ -195,13 +197,8 @@ export class WorkingCopies {
195
197
  return selected;
196
198
  }
197
199
  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
- ];
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 }]);
205
202
  const keys = new Set(selected.map((scope) => scope.key));
206
203
  const work = [];
207
204
  for (const key of this.copies.keys()) {
@@ -253,11 +250,11 @@ export class WorkingCopies {
253
250
  async push(cwd, selector, caller) {
254
251
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
252
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
253
+ return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
257
254
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
255
  const key = await this.keyAt(dirname(common));
259
256
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
257
+ return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
261
258
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
259
  }
263
260
  async keyAt(repository) {
@@ -285,7 +282,7 @@ export class WorkingCopies {
285
282
  }).catch(logged);
286
283
  }
287
284
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
285
+ const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
289
286
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
287
  const temporary = `${path}.${process.pid}`;
291
288
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -293,12 +290,12 @@ export class WorkingCopies {
293
290
  this.copies.set(copy.key, copy);
294
291
  return copy;
295
292
  }
296
- async read(selected) {
297
- const prefixes = prefixesOf(selected);
293
+ async read(handle) {
294
+ const prefixes = [rootOf(handle)];
298
295
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
296
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
297
  if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
298
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
302
299
  const [view] = bundle.scopes;
303
300
  if (view === undefined)
304
301
  return null;
@@ -341,13 +338,13 @@ export class WorkingCopies {
341
338
  return;
342
339
  }
343
340
  const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
341
+ prefixes: [rootOf(copy.room_handle)],
345
342
  });
346
343
  const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
344
  if (authority === undefined)
348
345
  return;
349
346
  if (authority.managed_replication !== 'admitted') {
350
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
347
+ logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
351
348
  return;
352
349
  }
353
350
  if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
@@ -358,12 +355,12 @@ export class WorkingCopies {
358
355
  await this.save(await this.caughtUp(copy));
359
356
  }
360
357
  async materialize(scope) {
361
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
358
+ const read = await this.read(scope.handle);
362
359
  if (read === null) {
363
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
360
+ logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
364
361
  return;
365
362
  }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
363
+ const repository = join(localCopies(), 'rooms', read.anchor);
367
364
  if (await exists(repository)) {
368
365
  logged(`${repository} already exists and House Kit leaves it as it is`);
369
366
  return;
@@ -375,7 +372,7 @@ export class WorkingCopies {
375
372
  try {
376
373
  await git(staging, ['init', '-q', '-b', BRANCH]);
377
374
  house = await importCommits(staging, [
378
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
375
+ { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
379
376
  ]);
380
377
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
381
378
  await git(staging, ['reset', '-q', '--hard']);
@@ -389,7 +386,6 @@ export class WorkingCopies {
389
386
  }
390
387
  await this.save({
391
388
  key: scope.key,
392
- private: scope.private,
393
389
  room_ref: read.anchor,
394
390
  room_handle: scope.handle,
395
391
  repository,
@@ -405,11 +401,11 @@ export class WorkingCopies {
405
401
  });
406
402
  }
407
403
  async received(copy) {
408
- const read = await this.read(copy);
404
+ const read = await this.read(copy.room_handle);
409
405
  if (read === null)
410
406
  return copy;
411
407
  const house = await importCommits(copy.repository, [
412
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
408
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
413
409
  ]);
414
410
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
415
411
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -432,17 +428,16 @@ export class WorkingCopies {
432
428
  let answer;
433
429
  try {
434
430
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
431
+ prefixes: [rootOf(copy.room_handle)],
436
432
  positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
437
433
  });
438
434
  }
439
435
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
436
+ const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
441
437
  if (code === 'position_expired')
442
438
  return this.received(copy);
443
439
  if (code !== 'replica_behind')
444
440
  throw error;
445
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
446
441
  return copy;
447
442
  }
448
443
  const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
@@ -482,7 +477,7 @@ export class WorkingCopies {
482
477
  let { house, manifests } = copy;
483
478
  if (files.size > 0) {
484
479
  house = await importCommits(copy.repository, [
485
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
480
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
486
481
  ]);
487
482
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
488
483
  }
@@ -512,8 +507,9 @@ export class WorkingCopies {
512
507
  }
513
508
  copy = await this.save(without(copy));
514
509
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
510
+ if (!copy.active) {
511
+ return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
512
+ }
517
513
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
514
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
515
  if (base === undefined) {
@@ -546,7 +542,7 @@ export class WorkingCopies {
546
542
  return this.settle(submitted, caller);
547
543
  }
548
544
  async derived(copy, base, commit) {
549
- const root = rootOf(copy);
545
+ const root = rootOf(copy.room_handle);
550
546
  const before = await tree(copy.repository, base);
551
547
  const after = await tree(copy.repository, commit);
552
548
  const moved = movedPaths(copy.moves, before, after);
@@ -582,9 +578,8 @@ export class WorkingCopies {
582
578
  async settle(start, caller, restaged = false) {
583
579
  let copy = start;
584
580
  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
- }
581
+ if (push.caller !== caller.key)
582
+ return this.recovered(copy);
588
583
  let outcome;
589
584
  try {
590
585
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +587,10 @@ export class WorkingCopies {
592
587
  });
593
588
  }
594
589
  catch (error) {
590
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
591
  return {
596
592
  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.`,
593
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
594
  };
599
595
  }
600
596
  if ('refused' in outcome) {
@@ -603,31 +599,37 @@ export class WorkingCopies {
603
599
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
600
  }
605
601
  if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
602
+ return this.recovered(copy);
603
+ const rebased = rebaseFixes(outcome.refused);
607
604
  let settled = await this.save(without(copy));
608
605
  try {
609
606
  settled = await this.save(await this.caughtUp(settled));
610
607
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
608
+ catch {
609
+ return {
610
+ refused: true,
611
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
612
+ };
613
613
  }
614
614
  return {
615
615
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
616
+ text: rebased
617
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
618
+ : outcome.refused.text,
617
619
  };
618
620
  }
619
621
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
622
  }
621
- async recovered(copy, reason) {
623
+ async recovered(copy) {
622
624
  const push = copy.push;
623
625
  const current = await this.save(await this.caughtUp(copy));
624
626
  const house = await tree(copy.repository, current.house);
625
- const root = rootOf(current);
627
+ const root = rootOf(current.room_handle);
626
628
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
627
629
  await this.save(without(current));
628
630
  return {
629
631
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
632
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
633
  };
632
634
  }
633
635
  const written = [];
@@ -652,10 +654,10 @@ export class WorkingCopies {
652
654
  await this.save(without(copy));
653
655
  return { refused: false, text: integrated.text };
654
656
  }
655
- catch (error) {
657
+ catch {
656
658
  return {
657
659
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
660
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
661
  };
660
662
  }
661
663
  }
@@ -678,7 +680,7 @@ export class WorkingCopies {
678
680
  written = [
679
681
  ...written,
680
682
  {
681
- path: `${rootOf(copy)}/${path}`,
683
+ path: `${rootOf(copy.room_handle)}/${path}`,
682
684
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
683
685
  content: entry === undefined ? null : await blobText(copy.repository, entry, path),
684
686
  },
@@ -698,7 +700,7 @@ export class WorkingCopies {
698
700
  }
699
701
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
700
702
  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`;
703
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
702
704
  const house = await importCommits(copy.repository, [
703
705
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
704
706
  {
package/dist/end.js ADDED
@@ -0,0 +1,8 @@
1
+ import { killDescendants } from './scope.js';
2
+ export const REPLACED = 'another Kit started for this Environment';
3
+ export const SIGN_IN_AGAIN = 'sign in again';
4
+ export function end(reason) {
5
+ process.stderr.write(`kit: ${reason}\n`);
6
+ killDescendants();
7
+ process.exit(0);
8
+ }