@agentshouse/kit 0.2.1-caf8b8e5a324f → 0.2.1-cb345f4157685

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/acp.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import { Readable, Writable } from 'node:stream';
4
4
  import { CLIS, KIT_VERSION, adapterCommand, withoutProxy } from './clis.js';
5
5
  import { killTree } from './scope.js';
6
+ const RETIRED = 'the CLI was retired';
6
7
  export async function startAdapter(kind, cli, cwd, app, events = () => undefined, added = {}, signal, scope = null) {
7
8
  const adapter = CLIS[kind].adapter;
8
9
  const started = [adapter === null ? cli : adapterCommand(kind), CLIS[kind].args];
@@ -13,34 +14,53 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
13
14
  stdio: ['pipe', 'pipe', 'pipe'],
14
15
  detached: true,
15
16
  });
16
- const stop = () => killTree(child);
17
- signal?.addEventListener('abort', stop, { once: true });
18
- child.on('exit', () => signal?.removeEventListener('abort', stop));
17
+ let owned = true;
18
+ let retired = false;
19
19
  let stderr = '';
20
20
  child.stderr.on('data', (chunk) => {
21
21
  stderr = (stderr + chunk.toString('utf8')).slice(-4000);
22
22
  });
23
- const exited = new Promise((resolve) => {
24
- child.on('error', (error) => resolve(`the CLI did not start: ${error.message}`));
25
- child.on('exit', (code, signal) => {
26
- const last = stderr.trim().split('\n').at(-1);
27
- resolve(`the CLI exited with ${code ?? signal}${last ? `: ${last}` : ''}`);
28
- });
29
- });
30
23
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
31
24
  const connection = app.connect({
32
25
  writable: stream.writable,
33
26
  readable: stream.readable.pipeThrough(new TransformStream({
34
27
  transform(message, controller) {
35
28
  const read = message;
36
- events(read);
29
+ if (owned)
30
+ events(read);
37
31
  if (read.id !== undefined || (read.method !== 'session/update' && !read.method.startsWith('_'))) {
38
32
  controller.enqueue(message);
39
33
  }
40
34
  },
41
35
  })),
42
36
  });
43
- void exited.then((cause) => connection.close(new Error(cause)));
37
+ const { promise: lost, resolve: lose } = Promise.withResolvers();
38
+ const exited = new Promise((resolve) => {
39
+ const end = (cause) => {
40
+ if (owned)
41
+ lose(cause);
42
+ owned = false;
43
+ connection.close(new Error(cause));
44
+ resolve(cause);
45
+ };
46
+ child.on('error', (error) => end(`the CLI did not start: ${error.message}`));
47
+ child.on('exit', (code, signal) => {
48
+ const last = stderr.trim().split('\n').at(-1);
49
+ end(`the CLI exited with ${code ?? signal}${last ? `: ${last}` : ''}`);
50
+ });
51
+ });
52
+ const retire = () => {
53
+ owned = false;
54
+ if (retired)
55
+ return;
56
+ retired = true;
57
+ if (child.pid !== undefined)
58
+ killTree(child);
59
+ scope?.kill();
60
+ connection.close(new Error(RETIRED));
61
+ };
62
+ signal?.addEventListener('abort', retire, { once: true });
63
+ child.on('exit', () => signal?.removeEventListener('abort', retire));
44
64
  try {
45
65
  const initialized = await connection.agent.request('initialize', {
46
66
  protocolVersion: PROTOCOL_VERSION,
@@ -54,10 +74,10 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
54
74
  clientInfo: { name: '@agentshouse/kit', version: KIT_VERSION },
55
75
  });
56
76
  scope?.entered(child.pid);
57
- return { child, connection, initialized, exited };
77
+ return { connection, initialized, exited, lost, retire };
58
78
  }
59
79
  catch (error) {
60
- child.kill('SIGKILL');
80
+ retire();
61
81
  throw error;
62
82
  }
63
83
  }
package/dist/agents.js CHANGED
@@ -5,7 +5,6 @@ import { startAdapter } from './acp.js';
5
5
  import { CLIS, KIT_VERSION, below, installAdapter, installCli, locate, run, versionOf } from './clis.js';
6
6
  import { agentBase, kitHome } from './home.js';
7
7
  import { updatesItself } from './placement.js';
8
- import { killTree } from './scope.js';
9
8
  import { holdTranscription, transcriptionReport } from './transcription.js';
10
9
  import { KIT_WIRE } from './wire.js';
11
10
  // Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
@@ -109,8 +108,7 @@ async function offer(kind, cli, known, signal) {
109
108
  };
110
109
  }
111
110
  finally {
112
- if (adapter !== undefined)
113
- killTree(adapter.child);
111
+ adapter?.retire();
114
112
  }
115
113
  }
116
114
  function reported(kind, reading) {
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-caf8b8e5a324f",
3
+ "version": "0.2.1-cb345f4157685",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-caf8b8e5a324f/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-caf8b8e5a324f/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-caf8b8e5a324f/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/bridge.js CHANGED
@@ -81,7 +81,7 @@ function editAnswer(forwarded) {
81
81
  export async function openBridge(house, copies, conversationId, signal) {
82
82
  const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
83
83
  const origin = (await readEnrolment()).house;
84
- const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
84
+ const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
85
85
  const closed = new AbortController();
86
86
  const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
87
87
  const send = async (message, operation) => {
@@ -1,9 +1,10 @@
1
- import { client, } from '@agentclientprotocol/sdk';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
2
  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
6
  import { startAdapter } from './acp.js';
7
+ import { finalRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
10
  import { answeredFailure, readerFor } from './events.js';
@@ -11,14 +12,22 @@ import { placeFiles, uploadBytes } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
13
14
  import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
14
16
  import { killMarked, Scope } from './scope.js';
15
17
  import { holdSecretInput } from './secret-input.js';
16
18
  import { createHowWeWork } from './skills.js';
17
19
  import { transcribe } from './transcription.js';
18
20
  import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
19
22
  const KILLED = 'the conversation was killed';
20
23
  const REFUSED = 'the CLI refused the prompt';
21
- const NO_CONNECTION = "the Kit could not open this conversation's connection";
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
22
31
  const NO_START = "the Agent's CLI did not start";
23
32
  const NO_SESSION = "the Agent's CLI did not open a session";
24
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
@@ -30,9 +39,12 @@ async function step(reason, work) {
30
39
  return await work;
31
40
  }
32
41
  catch (error) {
33
- throw new NotStarted(reason, { cause: error });
42
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
34
43
  }
35
44
  }
45
+ function unbridged(error) {
46
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
+ }
36
48
  const EXIT_PLAN_MODE = {
37
49
  parse(value) {
38
50
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -45,6 +57,13 @@ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kil
45
57
  function causeOf(error) {
46
58
  return error instanceof Error ? error.message : String(error);
47
59
  }
60
+ function answeredWith(error) {
61
+ const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
+ return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
63
+ }
64
+ function exitedWith(running, cause) {
65
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
66
+ }
48
67
  function answerMessage(request, response) {
49
68
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
50
69
  }
@@ -111,9 +130,7 @@ function asksSecret(request) {
111
130
  export class Conversations {
112
131
  kit;
113
132
  conversations = new Map();
114
- acks = new Map();
115
133
  questions = new Map();
116
- carrying = 0;
117
134
  reporting = 0;
118
135
  watched = false;
119
136
  constructor(kit) {
@@ -122,37 +139,26 @@ export class Conversations {
122
139
  setInterval(() => this.look(), 2_000);
123
140
  }
124
141
  input(input) {
125
- this.carrying++;
126
- const carried = this.acks.get(input.input_id) ??
127
- (() => {
128
- const conversation = this.conversation(input.conversation_id);
129
- if (input.kind === 'kill') {
130
- conversation.kills++;
131
- conversation.killed.abort(new Error(KILLED));
132
- for (const waiting of conversation.waiting.splice(0))
133
- waiting.settle({ refused: KILLED });
134
- this.halt(conversation);
135
- }
136
- if (input.kind === 'interrupt') {
137
- conversation.interrupts++;
138
- conversation.interrupted.abort();
139
- }
140
- const handled = conversation.queue.then(() => this.carry(conversation, input));
141
- conversation.queue = handled.catch(() => undefined);
142
- const work = handled.then(({ ack }) => ack);
143
- this.acks.set(input.input_id, work);
144
- return work;
145
- })();
146
- carried
147
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
148
- .catch(logged)
149
- .finally(() => {
150
- this.carrying--;
151
- this.kit.changed();
142
+ void this.kit.inputs.carry(input.input_id, () => {
143
+ const conversation = this.conversation(input.conversation_id);
144
+ if (input.kind === 'kill') {
145
+ conversation.kills++;
146
+ conversation.killed.abort(new Error(KILLED));
147
+ for (const waiting of conversation.waiting.splice(0))
148
+ waiting.settle({ refused: KILLED });
149
+ this.halt(conversation);
150
+ }
151
+ if (input.kind === 'interrupt') {
152
+ conversation.interrupts++;
153
+ conversation.interrupted.abort();
154
+ }
155
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
156
+ conversation.queue = handled.catch(() => undefined);
157
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
152
158
  });
153
159
  }
154
160
  idle() {
155
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
156
162
  return false;
157
163
  for (const conversation of this.conversations.values()) {
158
164
  const turn = conversation.turn;
@@ -387,7 +393,7 @@ export class Conversations {
387
393
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
388
394
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
389
395
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
390
- const bridge = await step(NO_CONNECTION, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
396
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
391
397
  if (conversation.kills > 0) {
392
398
  bridge.close();
393
399
  throw new Error(KILLED);
@@ -395,11 +401,12 @@ export class Conversations {
395
401
  conversation.opening = bridge;
396
402
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
397
403
  const scope = new Scope(marker(bridge), randomUUID(), cli);
404
+ let adapter = null;
398
405
  try {
399
406
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
400
407
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
401
408
  const streamed = await step(NO_CONNECTION, work.open(cli));
402
- const adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
409
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
403
410
  work.read(message);
404
411
  this.read(conversation, reader, message);
405
412
  }, { ...bridge.env, ...streamed }, undefined, scope));
@@ -423,11 +430,11 @@ export class Conversations {
423
430
  if (conversation.commands !== null)
424
431
  this.commands(conversation, conversation.commands);
425
432
  this.options(conversation, options);
426
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
427
434
  return running.sessionId;
428
435
  }
429
436
  catch (error) {
430
- scope.kill();
437
+ adapter?.retire();
431
438
  work.close();
432
439
  conversation.leftovers.add(scope);
433
440
  bridge.close();
@@ -470,13 +477,10 @@ export class Conversations {
470
477
  }
471
478
  return options;
472
479
  }
473
- exited(conversation, adapter, cause) {
474
- const running = conversation.running;
475
- if (running?.adapter !== adapter)
476
- return;
480
+ exited(conversation, running, cause) {
477
481
  this.detach(conversation, running);
478
482
  if (conversation.turn !== null) {
479
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
483
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
480
484
  }
481
485
  this.next(conversation);
482
486
  this.kit.changed();
@@ -543,6 +547,8 @@ export class Conversations {
543
547
  end(conversation, turn, failed, notice = null) {
544
548
  if (turn.ended)
545
549
  return;
550
+ if (failed !== null)
551
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
546
552
  if (conversation.turn === turn) {
547
553
  for (const event of conversation.running?.reader.closeTurn() ?? [])
548
554
  this.apply(conversation, turn, event);
@@ -556,7 +562,7 @@ export class Conversations {
556
562
  const body = Promise.allSettled(turn.uploads).then(() => ({
557
563
  parts: turn.parts.saved(),
558
564
  context: turn.parts.context,
559
- ...failed,
565
+ ...(failed === null ? {} : { failed: failed.failure }),
560
566
  }));
561
567
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
562
568
  this.next(conversation);
@@ -569,18 +575,22 @@ export class Conversations {
569
575
  conversation.prompted = running.sessionId;
570
576
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
571
577
  const notice = answeredFailure(answer);
572
- const failed = notice ?? (answer.stopReason === 'refusal' ? REFUSED : null);
573
- this.end(conversation, turn, failed === null ? null : { failed }, notice);
578
+ if (notice !== null) {
579
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
580
+ }
581
+ else {
582
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
583
+ }
574
584
  }, async (error) => {
575
585
  if (running.adapter.connection.signal.aborted) {
576
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
586
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
577
587
  }
578
588
  else {
579
589
  if (conversation.open === 1 && conversation.running === running) {
580
- running.scope.kill();
590
+ running.adapter.retire();
581
591
  this.detach(conversation, running);
582
592
  }
583
- this.end(conversation, turn, { failed: causeOf(error) });
593
+ this.end(conversation, turn, answeredWith(error));
584
594
  }
585
595
  });
586
596
  }
package/dist/copies.js CHANGED
@@ -219,26 +219,35 @@ export class LocalCopies {
219
219
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
220
220
  .sort();
221
221
  }
222
- async connected() {
223
- const copied = await this.answered(() => this.fetched(false));
224
- if (copied.refused)
225
- logged(copied.text);
222
+ connected() {
223
+ return this.selected();
224
+ }
225
+ async changed(rooms) {
226
+ if (rooms.length > 0)
227
+ await this.selected(rooms);
226
228
  }
227
229
  fetch() {
228
230
  return this.answered(() => this.fetched(true));
229
231
  }
232
+ async selected(rooms) {
233
+ const copied = await this.answered(() => this.fetched(false, rooms));
234
+ if (copied.refused)
235
+ logged(copied.text);
236
+ }
230
237
  answered(work) {
231
238
  const answered = this.answering.then(work);
232
239
  this.answering = answered.catch(() => undefined);
233
240
  return answered;
234
241
  }
235
- async fetched(refreshing) {
236
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
242
+ async fetched(refreshing, rooms) {
243
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
237
244
  const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
238
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
245
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
246
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
247
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
239
248
  const said = [];
240
249
  let refused = false;
241
- for (const copy of [...this.copies.values()]) {
250
+ for (const copy of held()) {
242
251
  const returned = synced.has(copy.key);
243
252
  if (returned && copy.erasing === undefined)
244
253
  continue;
package/dist/events.js CHANGED
@@ -513,7 +513,8 @@ class GrokReader extends Reader {
513
513
  }
514
514
  }
515
515
  export function answeredFailure(answer) {
516
- return answer._meta?.jetbrains?.air?.sessionFailure?.title ?? null;
516
+ const failure = answer._meta?.jetbrains?.air?.sessionFailure;
517
+ return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
517
518
  }
518
519
  export function readerFor(kind) {
519
520
  if (kind === 'claude-agent-acp')
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,37 @@
1
+ function logged(error) {
2
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
3
+ }
4
+ export class Inputs {
5
+ house;
6
+ changed;
7
+ carried = new Map();
8
+ open = 0;
9
+ constructor(house, changed) {
10
+ this.house = house;
11
+ this.changed = changed;
12
+ }
13
+ idle() {
14
+ return this.open === 0;
15
+ }
16
+ carry(inputId, work) {
17
+ const known = this.carried.get(inputId);
18
+ const carried = known ?? work();
19
+ if (known === undefined) {
20
+ this.carried.set(inputId, carried);
21
+ carried.catch(() => this.carried.delete(inputId));
22
+ }
23
+ this.open++;
24
+ this.changed();
25
+ return carried
26
+ .then(async ({ ack, acknowledged }) => {
27
+ await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
28
+ if (known === undefined)
29
+ await acknowledged?.();
30
+ })
31
+ .catch(logged)
32
+ .finally(() => {
33
+ this.open--;
34
+ this.changed();
35
+ });
36
+ }
37
+ }
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/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/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-caf8b8e5a324f",
3
+ "version": "0.2.1-cb345f4157685",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {