@agentshouse/kit 0.2.1-c70284f7a7466 → 0.2.1-c7a397cb0cce7

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/files.js CHANGED
@@ -11,7 +11,7 @@ export function verified(bytes, sha256, name) {
11
11
  export async function placeFiles(house, directory, files, signal) {
12
12
  const paths = [];
13
13
  for (const file of files) {
14
- const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
14
+ const original = await retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
15
15
  const answer = await fetch(relayed(original.download.url), { signal });
16
16
  if (!answer.ok)
17
17
  throw new Error(`${file.name} did not download: ${answer.status}`);
package/dist/inputs.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { kitHome } from './home.js';
4
+ function logged(error) {
5
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
6
+ }
7
+ export class Inputs {
8
+ house;
9
+ changed;
10
+ carried = new Map();
11
+ path = join(kitHome(), 'handed-inputs.json');
12
+ handed;
13
+ answered = new Set();
14
+ open = 0;
15
+ constructor(house, changed) {
16
+ this.house = house;
17
+ this.changed = changed;
18
+ this.handed = new Map(existsSync(this.path) ? Object.entries(JSON.parse(readFileSync(this.path, 'utf8'))) : []);
19
+ }
20
+ hands(inputId, ack) {
21
+ if (this.answered.has(inputId))
22
+ return;
23
+ this.handed.set(inputId, ack);
24
+ this.save();
25
+ }
26
+ idle() {
27
+ return this.open === 0;
28
+ }
29
+ carry(inputId, work) {
30
+ const known = this.carried.get(inputId);
31
+ const handed = this.handed.get(inputId);
32
+ const carried = known ?? (handed === undefined ? work() : Promise.resolve({ ack: handed }));
33
+ if (known === undefined) {
34
+ this.carried.set(inputId, carried);
35
+ carried.catch(() => this.carried.delete(inputId));
36
+ }
37
+ this.open++;
38
+ this.changed();
39
+ return carried
40
+ .then(async ({ ack, acknowledged }) => {
41
+ try {
42
+ await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
43
+ }
44
+ finally {
45
+ this.answered.add(inputId);
46
+ if (this.handed.delete(inputId))
47
+ this.save();
48
+ }
49
+ if (known === undefined)
50
+ await acknowledged?.();
51
+ })
52
+ .catch(logged)
53
+ .finally(() => {
54
+ this.open--;
55
+ this.changed();
56
+ });
57
+ }
58
+ save() {
59
+ const temporary = `${this.path}.${process.pid}`;
60
+ writeFileSync(temporary, JSON.stringify(Object.fromEntries(this.handed)), { mode: 0o600 });
61
+ renameSync(temporary, this.path);
62
+ }
63
+ }
package/dist/resident.js CHANGED
@@ -12,6 +12,7 @@ import { CONVERSATION_INPUTS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
13
  import { restart } from './end.js';
14
14
  import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
15
+ import { Inputs } from './inputs.js';
15
16
  import { killDescendants } from './scope.js';
16
17
  import { quoted } from './shell.js';
17
18
  import { SignIns } from './sign-in.js';
@@ -133,7 +134,7 @@ export async function resident() {
133
134
  const agents = new Agents(house, () => changed());
134
135
  let stream = null;
135
136
  let reported = false;
136
- const idle = () => conversations.idle() && signIns.idle() && agents.idle();
137
+ const idle = () => inputs.idle() && conversations.idle() && agents.idle();
137
138
  const changed = () => {
138
139
  if (!idle()) {
139
140
  reported = false;
@@ -146,15 +147,17 @@ export async function resident() {
146
147
  logged(house.deliver('/kit/idle', {}, idle));
147
148
  }
148
149
  };
150
+ const inputs = new Inputs(house, changed);
149
151
  const conversations = new Conversations({
150
152
  house,
151
153
  agents,
152
154
  copies,
155
+ inputs,
153
156
  send: (frame) => stream?.send(frame) ?? false,
154
157
  changed,
155
158
  });
156
- const signIns = new SignIns(house, agents, changed);
157
- const updates = new Updates(house);
159
+ const signIns = new SignIns(house, agents, inputs);
160
+ const updates = new Updates(inputs);
158
161
  for (const signal of ['SIGTERM', 'SIGINT']) {
159
162
  process.once(signal, () => {
160
163
  killDescendants();
@@ -173,10 +176,11 @@ export async function resident() {
173
176
  frame: (arrived) => {
174
177
  if (arrived.type === 'watching')
175
178
  conversations.watching(arrived.watching === true);
179
+ if (arrived.type === 'authority_changed')
180
+ logged(copies.changed(arrived.rooms));
176
181
  if (arrived.type === 'work_available' && arrived.subject === 'agents')
177
182
  logged(agents.refresh());
178
183
  if (arrived.type === 'input') {
179
- reported = false;
180
184
  if (arrived.kind === 'sign_in')
181
185
  signIns.start(arrived);
182
186
  else if (arrived.kind === 'update')
@@ -185,7 +189,7 @@ export async function resident() {
185
189
  conversations.input(arrived);
186
190
  else {
187
191
  const refused = { refused: `This Kit does not know ${arrived.kind}; update the Kit` };
188
- logged(house.deliver(`/kit/inputs/${arrived.input_id}/ack`, refused).finally(changed));
192
+ void inputs.carry(String(arrived.input_id), async () => ({ ack: refused }));
189
193
  }
190
194
  }
191
195
  },
package/dist/scope.js CHANGED
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  const LISTING_BYTES = 1 << 28;
5
5
  const CGROUPS = '/sys/fs/cgroup';
6
6
  const GONE = new Set(['ENOENT', 'ESRCH']);
7
+ const EXITED = new Set(['Z', 'X']);
7
8
  function failure(error) {
8
9
  return error.code ?? '';
9
10
  }
@@ -24,6 +25,8 @@ function read(pid) {
24
25
  try {
25
26
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
26
27
  const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
28
+ if (EXITED.has(fields[0]))
29
+ return null;
27
30
  const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
28
31
  return { parent: Number(fields[1]), session: fields[3], command };
29
32
  }
package/dist/service.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { kitHome } from './home.js';
2
5
  const UNIT = 'house-kit.service';
3
6
  // launchctl and systemctl answer at once, so ten seconds only bounds a stuck service manager.
4
7
  const MANAGER_MS = 10_000;
@@ -12,18 +15,37 @@ function start(command, args) {
12
15
  const cause = started.error?.message ?? (started.stderr.trim() || `${command} exited with ${started.status ?? 'a signal'}`);
13
16
  throw new Error(`the House Kit service could not be started: ${cause}`);
14
17
  }
18
+ function physical(path) {
19
+ return existsSync(path) ? realpathSync(path) : resolve(path);
20
+ }
21
+ function systemdHome(environment) {
22
+ for (const [, quoted, bare] of environment.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
23
+ const assignment = bare ?? quoted.replace(/\\(.)/g, '$1');
24
+ if (assignment.startsWith('HOUSE_KIT_HOME='))
25
+ return assignment.slice('HOUSE_KIT_HOME='.length);
26
+ }
27
+ return undefined;
28
+ }
29
+ function runsThisHome(served) {
30
+ return served !== undefined && physical(served) === physical(kitHome());
31
+ }
15
32
  export function startService() {
16
33
  if (process.platform === 'darwin') {
17
34
  const service = `gui/${process.getuid()}/house-kit`;
18
35
  const printed = manage('launchctl', ['print', service]);
19
36
  if (printed.status !== 0 || /^\tstate = running$/m.test(printed.stdout))
20
37
  return false;
38
+ if (!runsThisHome(/^\t+HOUSE_KIT_HOME => (.+)$/m.exec(printed.stdout)?.[1]))
39
+ return false;
21
40
  start('launchctl', ['kickstart', service]);
22
41
  return true;
23
42
  }
24
43
  for (const scope of [['--user'], []]) {
25
44
  if (manage('systemctl', [...scope, 'is-enabled', '--quiet', UNIT]).status !== 0)
26
45
  continue;
46
+ const environment = manage('systemctl', [...scope, 'show', '--property=Environment', '--value', UNIT]).stdout;
47
+ if (!runsThisHome(systemdHome(environment)))
48
+ continue;
27
49
  if (manage('systemctl', [...scope, 'is-active', '--quiet', UNIT]).status === 0)
28
50
  return false;
29
51
  start('systemctl', [...scope, 'start', UNIT]);
package/dist/sign-in.js CHANGED
@@ -9,36 +9,20 @@ const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
9
9
  function causeOf(error) {
10
10
  return error instanceof Error ? error.message : String(error);
11
11
  }
12
- function logged(error) {
13
- process.stderr.write(`kit: ${causeOf(error)}\n`);
14
- }
15
12
  function endOf(signal) {
16
13
  return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
17
14
  }
18
15
  export class SignIns {
19
16
  house;
20
17
  agents;
21
- changed;
22
- running = new Set();
23
- constructor(house, agents, changed) {
18
+ inputs;
19
+ constructor(house, agents, inputs) {
24
20
  this.house = house;
25
21
  this.agents = agents;
26
- this.changed = changed;
27
- }
28
- idle() {
29
- return this.running.size === 0;
22
+ this.inputs = inputs;
30
23
  }
31
24
  start(input) {
32
- if (this.running.has(input.input_id))
33
- return;
34
- this.running.add(input.input_id);
35
- this.changed();
36
- this.signIn(input)
37
- .catch(logged)
38
- .finally(() => {
39
- this.running.delete(input.input_id);
40
- this.changed();
41
- });
25
+ void this.inputs.carry(input.input_id, () => this.signIn(input));
42
26
  }
43
27
  async signIn(input) {
44
28
  const ending = new AbortController();
@@ -48,9 +32,7 @@ export class SignIns {
48
32
  clearTimeout(timer);
49
33
  if (ended)
50
34
  process.stderr.write(`kit: the ${input.cli} sign-in reached the end House set for it\n`);
51
- await this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {});
52
- if (!ended)
53
- await this.agents.reread(input.cli);
35
+ return ended ? { ack: {} } : { ack: {}, acknowledged: () => this.agents.reread(input.cli) };
54
36
  }
55
37
  async signedIn(input, signal) {
56
38
  await this.login(input, signal);
package/dist/stream.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { subscribe } from 'node:diagnostics_channel';
2
- import { finalRefusal, HouseRefusal, retryDelay } from './api.js';
2
+ import { HouseRefusal, retryDelay } from './api.js';
3
3
  import { end, REPLACED, signInAgain, updateRequired } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
@@ -82,9 +82,9 @@ export function holdStream(handlers) {
82
82
  }
83
83
  if (refused !== null && tooOld(refused.text))
84
84
  updateRequired();
85
- if (finalRefusal(refused))
86
- end(refused.message);
87
- // A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused as retryable or left unanswered backs off on the jittered curve.
85
+ if (!opened && refused !== null)
86
+ process.stderr.write(`kit: ${refused.message}\n`);
87
+ // A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused or left unanswered backs off on the jittered curve, retryable or not, since a resident outlives a House deploy.
88
88
  setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
89
89
  };
90
90
  opening.onopen = () => {
package/dist/translate.js CHANGED
@@ -3,14 +3,8 @@ function same(left, right) {
3
3
  return JSON.stringify(left) === JSON.stringify(right);
4
4
  }
5
5
  function applied(document, changes) {
6
- let current = document;
7
- for (const change of changes) {
8
- const result = apply(current, change);
9
- if (result.outcome !== 'applied')
10
- return null;
11
- current = result.document;
12
- }
13
- return serialize(current);
6
+ const result = apply(document, changes);
7
+ return result.outcome === 'applied' ? serialize(result.document) : null;
14
8
  }
15
9
  function components(path, before, after, text) {
16
10
  const edits = [];
package/dist/update.js CHANGED
@@ -6,9 +6,6 @@ import { join } from 'node:path';
6
6
  import { run } from './clis.js';
7
7
  import { kitHome, workspaceRoot } from './home.js';
8
8
  import { service, throughSudo } from './placement.js';
9
- function logged(error) {
10
- process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
11
- }
12
9
  function directory() {
13
10
  return join(kitHome(), 'update');
14
11
  }
@@ -50,6 +47,12 @@ async function recorded() {
50
47
  return null;
51
48
  return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
52
49
  }
50
+ async function forget(inputId) {
51
+ if ((await pendingInput()) !== inputId)
52
+ return;
53
+ await rm(pending(), { force: true });
54
+ await rm(outcome(), { force: true });
55
+ }
53
56
  async function finished(inputId) {
54
57
  for (;;) {
55
58
  const ack = await recorded();
@@ -90,27 +93,28 @@ function rerun(script, inputId, flags) {
90
93
  });
91
94
  }
92
95
  export class Updates {
93
- house;
94
- held = new Set();
96
+ inputs;
95
97
  queue = Promise.resolve();
96
- constructor(house) {
97
- this.house = house;
98
+ constructor(inputs) {
99
+ this.inputs = inputs;
98
100
  }
99
101
  async resumed() {
100
102
  const inputId = await pendingInput();
101
103
  if (inputId === null)
102
104
  return;
103
- await this.acknowledge(inputId, await finished(inputId));
105
+ await this.enqueue(inputId, () => finished(inputId));
104
106
  }
105
107
  start(input) {
106
- if (this.held.has(input.input_id))
107
- return;
108
- this.held.add(input.input_id);
109
- this.queue = this.queue
110
- .then(() => this.carry(input))
111
- .then((ack) => this.acknowledge(input.input_id, ack))
112
- .catch(logged)
113
- .finally(() => this.held.delete(input.input_id));
108
+ void this.enqueue(input.input_id, () => this.carry(input));
109
+ }
110
+ enqueue(inputId, work) {
111
+ const before = this.queue;
112
+ const carried = this.inputs.carry(inputId, async () => {
113
+ await before;
114
+ return { ack: await work(), acknowledged: () => forget(inputId) };
115
+ });
116
+ this.queue = before.then(() => carried);
117
+ return carried;
114
118
  }
115
119
  async carry(input) {
116
120
  if ((await pendingInput()) === input.input_id)
@@ -131,11 +135,4 @@ export class Updates {
131
135
  refused: 'the Kit bootstrap did not finish',
132
136
  };
133
137
  }
134
- async acknowledge(inputId, ack) {
135
- await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
136
- if ((await pendingInput()) !== inputId)
137
- return;
138
- await rm(pending(), { force: true });
139
- await rm(outcome(), { force: true });
140
- }
141
138
  }
package/dist/wire.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { refusalOf } from './refusals.js';
2
- export const KIT_WIRE = 1;
2
+ export const KIT_WIRE = 2;
3
3
  export const KIT_WIRE_HEADER = 'x-house-kit-wire';
4
4
  export function tooOld(text) {
5
5
  return refusalOf(text)?.code === 'kit_update_required';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c70284f7a7466",
3
+ "version": "0.2.1-c7a397cb0cce7",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@agentclientprotocol/sdk": "1.7.0",
31
- "@agentshouse/mdmodel": "0.1.0-c68e2039f1ca8",
31
+ "@agentshouse/mdmodel": "0.1.0-cabd61587647f",
32
32
  "yaml": "2.9.1"
33
33
  },
34
34
  "peerDependencies": {