@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/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,12 +5,12 @@ 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.
12
11
  const MODEL_PROBE_QUIET_MS = 2_000;
13
12
  const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
13
+ const INSTALLING = { release: null, failure: null, installing: true, ...SIGNED_OUT };
14
14
  function select(options, category) {
15
15
  const option = options.find((candidate) => candidate.category === category);
16
16
  return option?.type === 'select' ? option : undefined;
@@ -109,8 +109,7 @@ async function offer(kind, cli, known, signal) {
109
109
  };
110
110
  }
111
111
  finally {
112
- if (adapter !== undefined)
113
- killTree(adapter.child);
112
+ adapter?.retire();
114
113
  }
115
114
  }
116
115
  function reported(kind, reading) {
@@ -119,6 +118,7 @@ function reported(kind, reading) {
119
118
  release: reading.release,
120
119
  ...(reading.minimum === undefined ? {} : { minimum: reading.minimum }),
121
120
  failure: reading.failure,
121
+ ...(reading.installing === undefined ? {} : { installing: reading.installing }),
122
122
  signed_in: reading.signed_in,
123
123
  models: reading.models,
124
124
  modes: reading.modes,
@@ -164,6 +164,7 @@ export class Agents {
164
164
  reading = Promise.resolve();
165
165
  queues = new Map();
166
166
  installing = Promise.resolve();
167
+ installs = new Map();
167
168
  readings = new Map();
168
169
  reads = new Map();
169
170
  starts = 0;
@@ -219,7 +220,7 @@ export class Agents {
219
220
  const { cli, release } = resolved;
220
221
  const known = this.readings.get(kind);
221
222
  const kept = known?.release === null || known?.minimum !== undefined ? SIGNED_OUT : (known ?? SIGNED_OUT);
222
- if (known?.release !== release || known.minimum !== undefined) {
223
+ if (known?.release !== release || known.minimum !== undefined || this.installs.has(kind)) {
223
224
  this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
224
225
  }
225
226
  const read = this.nextRead(kind);
@@ -234,7 +235,8 @@ export class Agents {
234
235
  });
235
236
  }
236
237
  located(kind, signal) {
237
- return this.queue(kind, () => this.ensure(kind, signal), signal);
238
+ const owner = {};
239
+ return this.queue(kind, () => this.ensure(kind, signal, owner).finally(() => this.settle(kind, owner)), signal);
238
240
  }
239
241
  async reread(kind) {
240
242
  await this.examine(kind, this.readings.get(kind) ?? SIGNED_OUT, offer);
@@ -242,7 +244,13 @@ export class Agents {
242
244
  }
243
245
  async recheck(kind, signal) {
244
246
  const known = this.readings.get(kind) ?? SIGNED_OUT;
245
- await this.examine(kind, known, async (kind, cli) => ((await signedIn(kind, cli, known, signal)) ? { ...known, signed_in: true } : SIGNED_OUT), signal);
247
+ const owner = {};
248
+ try {
249
+ await this.examine(kind, known, async (kind, cli) => ((await signedIn(kind, cli, known, signal)) ? { ...known, signed_in: true } : SIGNED_OUT), signal, owner);
250
+ }
251
+ finally {
252
+ this.settle(kind, owner);
253
+ }
246
254
  if (!signal.aborted)
247
255
  await this.report();
248
256
  }
@@ -256,8 +264,18 @@ export class Agents {
256
264
  this.reads.set(kind, read);
257
265
  return read;
258
266
  }
259
- update(kind, reading) {
267
+ hold(kind, reading) {
260
268
  this.readings.set(kind, reading);
269
+ this.installs.delete(kind);
270
+ }
271
+ update(kind, reading) {
272
+ this.hold(kind, reading);
273
+ this.report().catch(logged);
274
+ }
275
+ settle(kind, owner) {
276
+ if (this.installs.get(kind) !== owner)
277
+ return;
278
+ this.installs.delete(kind);
261
279
  this.report().catch(logged);
262
280
  }
263
281
  queue(kind, work, signal) {
@@ -268,13 +286,15 @@ export class Agents {
268
286
  this.queues.set(kind, queued.catch(() => undefined));
269
287
  return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
270
288
  }
271
- async ensure(kind, signal) {
289
+ async ensure(kind, signal, owner = {}) {
272
290
  const failed = await installAdapter(kind, signal);
273
291
  if (failed !== null)
274
292
  throw new Error(failed);
275
293
  const found = await locate(kind, signal);
276
294
  if (found !== null)
277
295
  return found;
296
+ this.installs.set(kind, owner);
297
+ this.report().catch(logged);
278
298
  const installing = this.installing.catch(() => undefined).then(() => installCli(kind, signal));
279
299
  this.installing = installing;
280
300
  const failure = await installing;
@@ -285,10 +305,10 @@ export class Agents {
285
305
  throw new Error(`the login shell does not find ${CLIS[kind].bin} after its install`);
286
306
  return installed;
287
307
  }
288
- async resolve(kind, signal) {
308
+ async resolve(kind, signal, owner) {
289
309
  let cli;
290
310
  try {
291
- cli = await realpath(await this.ensure(kind, signal));
311
+ cli = await realpath(await this.ensure(kind, signal, owner));
292
312
  }
293
313
  catch (error) {
294
314
  const failure = causeOf(error);
@@ -303,12 +323,12 @@ export class Agents {
303
323
  return { release, minimum, failure: null, ...SIGNED_OUT };
304
324
  return { cli, release };
305
325
  }
306
- async examine(kind, known, read, signal) {
326
+ async examine(kind, known, read, signal, owner) {
307
327
  const current = this.nextRead(kind);
308
- const resolved = await this.queue(kind, () => this.resolve(kind, signal), signal);
328
+ const resolved = await this.queue(kind, () => this.resolve(kind, signal, owner), signal);
309
329
  const reading = 'cli' in resolved ? { release: resolved.release, failure: null, ...(await read(kind, resolved.cli, known)) } : resolved;
310
330
  if (this.reads.get(kind) === current && !signal?.aborted)
311
- this.readings.set(kind, reading);
331
+ this.hold(kind, reading);
312
332
  }
313
333
  async quietOffer(kind, cli, known) {
314
334
  for (let seen = -1; seen !== this.starts;) {
@@ -326,7 +346,7 @@ export class Agents {
326
346
  }
327
347
  async send() {
328
348
  const agents = this.desired.agents.flatMap((kind) => {
329
- const reading = this.readings.get(kind);
349
+ const reading = this.installs.has(kind) ? INSTALLING : this.readings.get(kind);
330
350
  return reading === undefined ? [] : [reported(kind, reading)];
331
351
  });
332
352
  const hostKey = await sshHostKey();
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c70284f7a7466",
3
+ "version": "0.2.1-c7a397cb0cce7",
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-c70284f7a7466/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c7a397cb0cce7/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-c70284f7a7466/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c7a397cb0cce7/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-c70284f7a7466/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c7a397cb0cce7/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) => {