@agentshouse/kit 0.2.1-c3304109525e3 → 0.2.1-c33a81a492b90

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/README.md CHANGED
@@ -9,25 +9,25 @@ Run `sh -c 'kit=$(curl -fsSL "$0") || { echo "Kit could not be downloaded; run t
9
9
  or `& { try { $kit = irm agents.house/kit.ps1 } catch { throw 'Kit could not be downloaded; run this command again.' }; & ([scriptblock]::Create($kit)) @args }` on Windows, which run nothing of a failed download, and again to update; options follow `agents.house/kit`.
10
10
  On macOS and Linux it installs Kit natively under your own account, without an admin password, as a service that runs
11
11
  while you are logged in. `kit` and `house` are then on your login shell's `PATH`, and your workspace is `~/AgentsHouse` unless you
12
- pass `--workspace <path>`.
12
+ pass `--workspace <path>`. Every run places the Kit release your Building serves, with the Node.js and agent adapters it runs on, in a directory of its own beside the one that runs, then starts it; the earlier one stays until the next release. Kit also updates itself: when your Building takes another release, Kit places it the same way and, once no turn is open, ends so its service, or the container's restart policy, starts it. When House refuses the Kit that runs as too old or too new, Kit places your Building's release at once, ending any running turn, and ends the same way; while it cannot, it stays offline, prints why with the command that installs it again, and tries again regularly.
13
13
 
14
- Add `--container` to run Kit in one Docker container instead, with your
15
- workspace mounted at `/agents/house`; your Agents then reach only that
16
- workspace. On Windows 11 the container is the one choice. Append a `kit` or `house` command to the container's options to run it in that container.
14
+ Add `--container` to run Kit in one Docker container instead, with your workspace mounted at `/agents/house`; your Agents then reach only that
15
+ workspace. The container's image holds no Kit release and no Node.js: every run places them in Kit home's `kit` folder, which a container created again keeps. On Windows,
16
+ Kit home is the Docker volume `house-kit-home` and the container is the one choice. Append a `kit` or `house` command to the container's options to run it in that container.
17
17
  Running the command for the other choice on a connected computer asks you to
18
18
  confirm, then replaces its Environment, whose Agents end, with a new one. Running it with a different `HOUSE_KIT_HOME`
19
19
  from the running Kit process asks you to confirm before it replaces that process, and leaves the earlier Kit home as it was.
20
20
 
21
- On your own Linux server, run the Linux command with `--linux` to
22
- install Kit natively, with its workspace at `/agents/house`; `kit` and
23
- `house` are then on the server's `PATH`.
21
+ On your own Linux server, run the Linux command with `--linux` to install Kit natively, with its workspace at `/agents/house` and a missing `ca-certificates`, `curl` or `git` installed through apt; `kit` and `house` are then on the server's `PATH`.
24
22
 
25
- Kit runs the agent CLIs your login shell finds and installs a missing one
26
- through that CLI's own installer. A CLI older than the oldest release Kit is built for still runs, though its conversations may fail: Kit tells House its version, that oldest release and the CLI's own update command, such as `claude update`. Running the command again offers to update
27
- each CLI older than Kit needs; add `--update-clis` to update every one to
28
- its latest release without asking. Kit also gives every Agent on the
29
- Environment the agent skills of `@agentshouse/skills`, at the version each Kit
30
- release installs. Add `--no-skills` to the command to leave them out; running it again without the flag brings them back.
23
+ Kit is built for macOS 27 on Apple silicon, Ubuntu 26.04 LTS on amd64 and Windows 11 on amd64. On another release, distribution or architecture the command prints one warning line, such as `Warning: macOS 26.7.1 is outdated; update to macOS 27.`, and installs anyway. It refuses only an architecture other than amd64 and arm64, and a native install on a Linux without glibc, where `--container` still runs Kit. On a Linux without systemd it installs and connects Kit but starts nothing: run the `kit resident` command it prints under your own service manager, restarting it whenever it ends with a failure. `--container` installs Docker Engine on Ubuntu and Debian; another Linux needs a working Docker Engine first.
24
+
25
+ Kit runs each agent CLI through the link your login shell finds and installs a missing one through that CLI's own
26
+ installer. When it starts and regularly after, Kit runs each CLI's own update command, such as `claude update`, beside
27
+ your conversations: a running conversation keeps its version and the next one runs the new release. A CLI older than
28
+ the oldest release Kit is built for still runs, though its conversations may fail: Kit tells House its version, that
29
+ oldest release and its update command. Kit also gives every Agent on the Environment the agent skills of
30
+ `@agentshouse/skills`, at the version each Kit release installs. Add `--no-skills` to the command to leave them out; running it again without the flag brings them back.
31
31
 
32
32
  ## Commands
33
33
 
package/dist/afk.js ADDED
@@ -0,0 +1,17 @@
1
+ import { CLIS } from './clis.js';
2
+ const NOBODY_THERE = 'Nobody is there to answer. Decide for yourself and go on.';
3
+ export const PLAN_APPROVED = { outcome: 'approved' };
4
+ function told(fields) {
5
+ return Object.fromEntries(fields.map((field) => [field, NOBODY_THERE]));
6
+ }
7
+ export function permissionAllowed({ options }) {
8
+ const option = options.find(({ kind }) => kind === 'allow_once') ?? options.find(({ kind }) => kind === 'allow_always');
9
+ return { outcome: { outcome: 'selected', optionId: option.optionId } };
10
+ }
11
+ export function formAnswered(kind, request) {
12
+ const fields = CLIS[kind].questionFields(request);
13
+ return fields.length === 0 ? { action: 'decline' } : { action: 'accept', content: told(fields) };
14
+ }
15
+ export function grokQuestionAnswered({ questions }) {
16
+ return { outcome: 'accepted', answers: told(questions.map(({ question }) => question)) };
17
+ }
package/dist/agents.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { client } from '@agentclientprotocol/sdk';
2
- import { readFile, realpath } from 'node:fs/promises';
2
+ import { readFile } from 'node:fs/promises';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
4
  import { signedOut, startAdapter } from './acp.js';
5
- import { CLIS, KIT_RELEASE, below, installAdapter, installCli, locate, run, versionOf } from './clis.js';
5
+ import { CLIS, KIT_RELEASE, below, installCli, locate, run, updateCli, versionOf } from './clis.js';
6
6
  import { agentBase, kitHome } from './home.js';
7
- import { updatesItself } from './placement.js';
8
7
  import { holdTranscription, transcriptionReport } from './transcription.js';
9
8
  import { KIT_WIRE } from './wire.js';
10
9
  // Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
11
10
  const MODEL_PROBE_QUIET_MS = 2_000;
11
+ // A CLI's maker can publish several releases a day, so running each chosen CLI's update command hourly brings a new one to the next conversation within the hour for one small check.
12
+ export const CLI_UPDATE_MS = 60 * 60_000;
12
13
  // A sign-in's model read that has not answered in ten seconds leaves its sign-in to the status check, so a hung CLI never holds the sign-in's result.
13
14
  const SIGNED_IN_READ_MS = 10_000;
14
15
  const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
@@ -165,13 +166,15 @@ export class Agents {
165
166
  sending = Promise.resolve();
166
167
  sent = null;
167
168
  refreshing = 0;
169
+ loaded = Promise.resolve();
170
+ updating = null;
168
171
  changed;
169
172
  constructor(house, changed = () => undefined) {
170
173
  this.house = house;
171
174
  this.changed = changed;
172
175
  }
173
176
  idle() {
174
- return this.refreshing === 0;
177
+ return this.refreshing === 0 && this.updating === null;
175
178
  }
176
179
  read() {
177
180
  return this.reading.then(() => undefined, () => undefined);
@@ -187,7 +190,7 @@ export class Agents {
187
190
  return this.desired;
188
191
  });
189
192
  this.reading = reading;
190
- reading
193
+ this.loaded = reading
191
194
  .then(({ agents, transcribe }) => Promise.all([
192
195
  holdTranscription(transcribe)
193
196
  .then((changed) => (changed ? this.report() : undefined))
@@ -203,6 +206,10 @@ export class Agents {
203
206
  });
204
207
  return reading.then(() => undefined);
205
208
  }
209
+ keepCurrent() {
210
+ this.updateAll(this.loaded);
211
+ setInterval(() => this.updateAll(Promise.resolve()), CLI_UPDATE_MS);
212
+ }
206
213
  cli(kind) {
207
214
  this.starts += 1;
208
215
  return this.queue(kind, async () => {
@@ -281,9 +288,6 @@ export class Agents {
281
288
  return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
282
289
  }
283
290
  async ensure(kind, signal, owner = {}) {
284
- const failed = await installAdapter(kind, signal);
285
- if (failed !== null)
286
- throw new Error(failed);
287
291
  const found = await locate(kind, signal);
288
292
  if (found !== null)
289
293
  return found;
@@ -302,7 +306,7 @@ export class Agents {
302
306
  async resolve(kind, signal, owner) {
303
307
  let cli;
304
308
  try {
305
- cli = await realpath(await this.ensure(kind, signal, owner));
309
+ cli = await this.ensure(kind, signal, owner);
306
310
  }
307
311
  catch (error) {
308
312
  const failure = causeOf(error);
@@ -321,6 +325,35 @@ export class Agents {
321
325
  if (this.reads.get(kind) === current && !signal?.aborted)
322
326
  this.hold(kind, reading);
323
327
  }
328
+ updateAll(after) {
329
+ if (this.updating !== null)
330
+ return;
331
+ this.updating = after
332
+ .then(async () => {
333
+ for (const kind of this.desired.agents) {
334
+ if (this.installs.has(kind) || !this.readings.get(kind)?.release)
335
+ continue;
336
+ const failure = await updateCli(kind);
337
+ if (failure !== null)
338
+ process.stderr.write(`kit: ${kind} did not update: ${failure}\n`);
339
+ await this.readVersion(kind);
340
+ }
341
+ })
342
+ .catch(logged)
343
+ .finally(() => {
344
+ this.updating = null;
345
+ this.changed();
346
+ });
347
+ this.changed();
348
+ }
349
+ async readVersion(kind) {
350
+ const cli = await locate(kind);
351
+ const release = cli === null ? null : await versionOf(cli);
352
+ const current = this.readings.get(kind);
353
+ if (release === null || !current?.release || current.release === release)
354
+ return;
355
+ this.update(kind, { ...current, release });
356
+ }
324
357
  async quietOffer(kind, cli, known) {
325
358
  for (let seen = -1; seen !== this.starts;) {
326
359
  seen = this.starts;
@@ -347,7 +380,6 @@ export class Agents {
347
380
  ...(form === undefined ? {} : { form }),
348
381
  kit_release: KIT_RELEASE,
349
382
  wire: KIT_WIRE,
350
- updates_itself: await updatesItself(),
351
383
  agent_base: agentBase(),
352
384
  agents,
353
385
  transcription: transcriptionReport(),
package/dist/building.js CHANGED
@@ -16,10 +16,14 @@ async function served(name, signal) {
16
16
  await answer.body?.cancel();
17
17
  throw new Error(`House answered ${answer.status} for ${name}`);
18
18
  }
19
- export async function servedDigest(name, signal) {
19
+ export async function servedRelease(signal) {
20
20
  const manifest = (await (await served(MANIFEST, signal)).json());
21
- const sha256 = manifest.files?.[name]?.sha256;
22
- return typeof sha256 === 'string' ? sha256 : null;
21
+ const installer = manifest.hosts?.linux;
22
+ const sha256 = typeof installer === 'string' ? manifest.files?.[installer]?.sha256 : undefined;
23
+ if (typeof manifest.version !== 'string' || typeof installer !== 'string' || typeof sha256 !== 'string') {
24
+ throw new Error(`House serves no Kit release in ${MANIFEST}`);
25
+ }
26
+ return { version: manifest.version, installer, sha256 };
23
27
  }
24
28
  export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
25
29
  const answer = await served(name, signal);
package/dist/chunks.js ADDED
@@ -0,0 +1,49 @@
1
+ import { readFile, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { downloaded, fetched } from './files.js';
4
+ const DOWNLOADS_AT_ONCE = 6;
5
+ // A chunk is about one megabyte, so a download still unfinished after two minutes holds every Room behind it on a dead connection.
6
+ const CHUNK_DOWNLOAD_MS = 120_000;
7
+ class AddressExpired extends Error {
8
+ constructor() {
9
+ super("House's store refused every chunk it asked for");
10
+ }
11
+ }
12
+ function once(chunk) {
13
+ let asked = false;
14
+ return async () => {
15
+ if (asked)
16
+ throw new AddressExpired();
17
+ asked = true;
18
+ return { download: chunk.download, sha256: chunk.hash };
19
+ };
20
+ }
21
+ export async function stagedChunks(directory, chunks) {
22
+ let next = 0;
23
+ let failure;
24
+ const downloading = async () => {
25
+ while (next < chunks.length && failure === undefined) {
26
+ const chunk = chunks[next++];
27
+ const reading = (url) => fetched(url, AbortSignal.timeout(CHUNK_DOWNLOAD_MS));
28
+ await downloaded(once(chunk), reading, join(directory, chunk.hash), 'A Room chunk').catch((error) => {
29
+ failure ??= error;
30
+ });
31
+ }
32
+ };
33
+ await Promise.all(Array.from({ length: DOWNLOADS_AT_ONCE }, downloading));
34
+ return failure;
35
+ }
36
+ export async function* chunkTexts(directory, chunks, models) {
37
+ yield* models;
38
+ for (const { hash } of chunks) {
39
+ const file = join(directory, hash);
40
+ for (const line of (await readFile(file, 'utf8')).split('\n')) {
41
+ if (line === '')
42
+ continue;
43
+ const entry = JSON.parse(line);
44
+ if (entry.content !== undefined)
45
+ yield entry;
46
+ }
47
+ await rm(file);
48
+ }
49
+ }
package/dist/clis.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { constants, readFileSync } from 'node:fs';
4
- import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
5
  import { homedir, userInfo } from 'node:os';
6
6
  import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
- import { downloadServed, servedDigest } from './building.js';
8
- import { kitHome } from './home.js';
7
+ import { releaseDirectory } from './placement.js';
9
8
  import { childEnvironment } from './relay.js';
10
9
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
11
10
  function houseRule(houses) {
@@ -63,6 +62,14 @@ export async function allowHouseInCodex(path) {
63
62
  await rename(temporary, file);
64
63
  }
65
64
  async function nothing() { }
65
+ function claudeQuestionFields(request) {
66
+ const properties = request.requestedSchema?.properties ?? {};
67
+ return Object.keys(properties).filter((name) => properties[name]._meta?.jetbrains?.air?.customAnswer !== undefined);
68
+ }
69
+ function codexQuestionFields(request) {
70
+ const { _meta, requestedSchema } = request;
71
+ return _meta?.codex === undefined ? [] : (requestedSchema?.required ?? []);
72
+ }
66
73
  function answered(error) {
67
74
  return typeof error === 'object' && error !== null ? error : {};
68
75
  }
@@ -90,6 +97,7 @@ export const CLIS = {
90
97
  },
91
98
  renames: true,
92
99
  allowHouse: allowHouseInCodex,
100
+ questionFields: codexQuestionFields,
93
101
  signedIn: { command: ['login', 'status'] },
94
102
  login: { args: ['login', '--device-auth'], code: 'show' },
95
103
  air: ['asyncTasks', 'nativeSubagentSessions'],
@@ -121,6 +129,7 @@ export const CLIS = {
121
129
  unknownSession: (error) => answered(error).code === -32002,
122
130
  renames: false,
123
131
  allowHouse: nothing,
132
+ questionFields: claudeQuestionFields,
124
133
  signedIn: { command: ['auth', 'status'] },
125
134
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
126
135
  air: ['asyncTasks', 'sessionFailure'],
@@ -137,6 +146,7 @@ export const CLIS = {
137
146
  unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
138
147
  renames: false,
139
148
  allowHouse: nothing,
149
+ questionFields: () => [],
140
150
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
141
151
  login: { args: ['login', '--device-auth'], code: 'show' },
142
152
  air: ['asyncTasks'],
@@ -145,25 +155,13 @@ export const CLIS = {
145
155
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
146
156
  export const KIT_RELEASE = KIT_PACKAGE.version;
147
157
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
148
- // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
158
+ // An install or update still running after fifteen minutes is ended as failed, so a hung download can neither hold its CLI's report nor keep the Kit busy.
149
159
  const INSTALL_MS = 15 * 60_000;
150
160
  // A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
151
161
  const READ_MS = 30_000;
152
162
  const CAUSE_CHARACTERS = 4000;
153
- function prefix(kind) {
154
- return join(kitHome(), 'agents', kind);
155
- }
156
163
  export function adapterCommand(kind) {
157
- return join(prefix(kind), 'node_modules', '.bin', CLIS[kind].adapter.bin);
158
- }
159
- async function installedAdapter(kind) {
160
- try {
161
- const manifest = join(prefix(kind), 'node_modules', CLIS[kind].adapter.package, 'package.json');
162
- return JSON.parse(await readFile(manifest, 'utf8')).version;
163
- }
164
- catch {
165
- return null;
166
- }
164
+ return join(releaseDirectory(KIT_RELEASE), 'adapters', kind, 'node_modules', '.bin', CLIS[kind].adapter.bin);
167
165
  }
168
166
  export function run(command, args, timeoutMs, detached = false, signal) {
169
167
  return new Promise((resolve) => {
@@ -207,38 +205,15 @@ export function failureOf(ran, program) {
207
205
  export function adapterArchive(kind) {
208
206
  return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
209
207
  }
210
- export async function installAdapter(kind, signal) {
211
- const adapter = CLIS[kind].adapter;
212
- if (adapter === null)
213
- return null;
214
- if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
215
- return null;
216
- const name = adapterArchive(kind);
217
- const archive = `${prefix(kind)}.tar.gz`;
208
+ async function bounded(program, work, signal) {
218
209
  const deadline = AbortSignal.timeout(INSTALL_MS);
219
- const bounded = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
220
- try {
221
- const sha256 = await servedDigest(name, bounded);
222
- if (sha256 === null)
223
- return `House serves no ${name}`;
224
- await rm(prefix(kind), { recursive: true, force: true });
225
- await mkdir(prefix(kind), { recursive: true });
226
- await downloadServed(name, sha256, archive, bounded);
227
- const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
228
- return ran.status === 0 ? null : failureOf(ran, 'tar');
229
- }
230
- catch (error) {
231
- if (deadline.aborted && signal?.aborted !== true)
232
- return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
233
- return error instanceof Error ? error.message : String(error);
234
- }
235
- finally {
236
- await rm(archive, { force: true });
237
- }
210
+ const ran = await work(signal === undefined ? deadline : AbortSignal.any([signal, deadline]));
211
+ if (deadline.aborted && signal?.aborted !== true)
212
+ return `${program} did not end within ${INSTALL_MS / 1000} seconds`;
213
+ return ran.status === 0 ? null : failureOf(ran, program);
238
214
  }
239
- export async function installCli(kind, signal) {
240
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
241
- return ran.status === 0 ? null : failureOf(ran, 'the install');
215
+ export function installCli(kind, signal) {
216
+ return bounded('the install', (within) => run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, within), signal);
242
217
  }
243
218
  function loginShell() {
244
219
  return process.env.SHELL || userInfo().shell;
@@ -246,8 +221,8 @@ function loginShell() {
246
221
  const FOUND = 'house-kit-cli ';
247
222
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
248
223
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
249
- function inLoginShell(script, argument = '', signal) {
250
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
224
+ function inLoginShell(script, argument = '', signal, timeoutMs = READ_MS) {
225
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], timeoutMs, true, signal);
251
226
  }
252
227
  export async function onLoginPath(program, signal) {
253
228
  const ran = await inLoginShell(LOOKUP, program, signal);
@@ -256,6 +231,9 @@ export async function onLoginPath(program, signal) {
256
231
  export function locate(kind, signal) {
257
232
  return onLoginPath(CLIS[kind].bin, signal);
258
233
  }
234
+ export function updateCli(kind) {
235
+ return bounded('the update', (within) => inLoginShell(CLIS[kind].update, '', within, INSTALL_MS));
236
+ }
259
237
  export async function loginPath() {
260
238
  const marker = 'house-kit-path ';
261
239
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
@@ -4,7 +4,7 @@ import { end, REPLACED, signInAgain } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
6
  import { Silence } from './silence.js';
7
- import { endUnserved, KIT_WIRE } from './wire.js';
7
+ import { endUnserved, KIT_WIRE, unserved } from './wire.js';
8
8
  const PATH = '/kit/stream';
9
9
  const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
10
10
  const REFUSAL_HEADER = 'x-house-refusal';
@@ -32,11 +32,10 @@ subscribe('undici:request:headers', (message) => {
32
32
  export function holdConnection(handlers) {
33
33
  let connection = null;
34
34
  let socket = null;
35
- let paused = false;
36
35
  let failures = 0;
37
36
  const connect = async () => {
38
37
  const { house, credential } = (await readEnrolment());
39
- if (paused || socket !== null)
38
+ if (socket !== null || unserved())
40
39
  return;
41
40
  refused = null;
42
41
  const opening = new WebSocket(relayed(connectionUrl(house)), {
@@ -65,8 +64,6 @@ export function holdConnection(handlers) {
65
64
  endUnserved(refused.text);
66
65
  if (!opened && refused !== null)
67
66
  process.stderr.write(`kit: ${refused.message}\n`);
68
- if (paused)
69
- return;
70
67
  // A closed connection reopens in one to three seconds, spread so Kit processes 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.
71
68
  setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
72
69
  };
@@ -104,13 +101,5 @@ export function holdConnection(handlers) {
104
101
  connection.send(JSON.stringify(notification));
105
102
  return true;
106
103
  },
107
- pause() {
108
- paused = true;
109
- socket?.close();
110
- return () => {
111
- paused = false;
112
- void connect();
113
- };
114
- },
115
104
  };
116
105
  }
@@ -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 { signedOut, startAdapter } from './acp.js';
7
+ import { formAnswered, grokQuestionAnswered, permissionAllowed, PLAN_APPROVED } from './afk.js';
7
8
  import { finalRefusal, HouseRefusal } from './api.js';
8
9
  import { openBridge } from './bridge.js';
9
10
  import { CLIS } from './clis.js';
@@ -62,6 +63,11 @@ const EXIT_PLAN_MODE = {
62
63
  return value;
63
64
  },
64
65
  };
66
+ const GROK_QUESTION = {
67
+ parse(value) {
68
+ return value;
69
+ },
70
+ };
65
71
  export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
66
72
  function causeOf(error) {
67
73
  return error instanceof Error ? error.message : String(error);
@@ -128,6 +134,7 @@ class Conversation {
128
134
  commands = null;
129
135
  options = null;
130
136
  exitUntold = false;
137
+ afk = false;
131
138
  context = null;
132
139
  leftovers = new Set();
133
140
  waiting = [];
@@ -177,6 +184,8 @@ export class Conversations {
177
184
  call(call) {
178
185
  void this.kit.calls.carry(call.input_id, () => {
179
186
  const conversation = this.conversation(call.conversation_id);
187
+ if (call.kind === 'open' || call.kind === 'message')
188
+ conversation.afk = call.afk === true;
180
189
  if (call.kind === 'kill') {
181
190
  conversation.kills++;
182
191
  conversation.killed.abort(new Error(KILLED));
@@ -622,9 +631,14 @@ export class Conversations {
622
631
  const reader = readerFor(route.kind);
623
632
  reader.session = sessionId;
624
633
  const app = client({ name: '@agentshouse/kit' })
625
- .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
626
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
627
- .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
634
+ .onRequest('session/request_permission', ({ params, signal }) => conversation.afk ? permissionAllowed(params) : this.ask(conversation, 'session/request_permission', params, false, signal))
635
+ .onRequest('elicitation/create', ({ params, signal }) => conversation.afk ? formAnswered(route.kind, params) : this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
636
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => conversation.afk ? PLAN_APPROVED : this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
637
+ .onRequest('_x.ai/ask_user_question', GROK_QUESTION, ({ params }) => {
638
+ if (!conversation.afk)
639
+ throw RequestError.methodNotFound('_x.ai/ask_user_question');
640
+ return grokQuestionAnswered(params);
641
+ });
628
642
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
629
643
  const bridging = this.bridged(conversation);
630
644
  const bridge = await step(unbridged, bridging);