@agentshouse/kit 0.2.1-c3fc787e93f0e → 0.2.1-c44c14117631a

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.
Files changed (127) hide show
  1. package/README.md +30 -29
  2. package/dist/acp.js +38 -17
  3. package/dist/agents.js +87 -19
  4. package/dist/api.js +5 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +4 -7
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +7 -1
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +49 -16
  13. package/dist/connection.js +126 -0
  14. package/dist/conversations.js +404 -154
  15. package/dist/copies.js +77 -50
  16. package/dist/end.js +9 -4
  17. package/dist/events.js +6 -2
  18. package/dist/files.js +139 -37
  19. package/dist/git.js +5 -6
  20. package/dist/home.js +31 -13
  21. package/dist/house-cli.js +299 -0
  22. package/dist/house-main.js +7 -262
  23. package/dist/instructions.js +6 -10
  24. package/dist/kit-cli.js +68 -0
  25. package/dist/kit-main.js +8 -67
  26. package/dist/login.js +34 -11
  27. package/dist/logout.js +0 -2
  28. package/dist/mcp.js +4 -2
  29. package/dist/placement.js +9 -18
  30. package/dist/relay.js +21 -0
  31. package/dist/resident.js +88 -37
  32. package/dist/scope.js +3 -0
  33. package/dist/service.js +23 -1
  34. package/dist/sign-in.js +22 -39
  35. package/dist/skills.js +12 -34
  36. package/dist/transcription.js +85 -45
  37. package/dist/translate.js +2 -8
  38. package/dist/update.js +71 -48
  39. package/dist/web-wizard.js +101 -0
  40. package/dist/wire.js +19 -3
  41. package/dist/work.js +3 -5
  42. package/package.json +20 -20
  43. package/dist/bootstrap.json +0 -24
  44. package/dist/stream.js +0 -77
  45. package/how-we-work.md +0 -23
  46. package/skills/ask-matt/LICENSE +0 -21
  47. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  48. package/skills/ask-matt/SKILL.md +0 -91
  49. package/skills/ask-matt/agents/openai.yaml +0 -4
  50. package/skills/code-review/LICENSE +0 -21
  51. package/skills/code-review/SKILL.md +0 -89
  52. package/skills/code-review/agents/openai.yaml +0 -3
  53. package/skills/codebase-design/DEEPENING.md +0 -37
  54. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  55. package/skills/codebase-design/LICENSE +0 -21
  56. package/skills/codebase-design/SKILL.md +0 -116
  57. package/skills/codebase-design/agents/openai.yaml +0 -3
  58. package/skills/diagnosing-bugs/LICENSE +0 -21
  59. package/skills/diagnosing-bugs/SKILL.md +0 -140
  60. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  61. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  62. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  63. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  64. package/skills/domain-modeling/LICENSE +0 -21
  65. package/skills/domain-modeling/SKILL.md +0 -76
  66. package/skills/domain-modeling/agents/openai.yaml +0 -3
  67. package/skills/grill-me/LICENSE +0 -21
  68. package/skills/grill-me/SKILL.md +0 -8
  69. package/skills/grill-me/agents/openai.yaml +0 -4
  70. package/skills/grill-with-docs/LICENSE +0 -21
  71. package/skills/grill-with-docs/SKILL.md +0 -8
  72. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  73. package/skills/grilling/LICENSE +0 -21
  74. package/skills/grilling/SKILL.md +0 -30
  75. package/skills/grilling/agents/openai.yaml +0 -3
  76. package/skills/handoff/LICENSE +0 -21
  77. package/skills/handoff/SKILL.md +0 -17
  78. package/skills/handoff/agents/openai.yaml +0 -4
  79. package/skills/implement/LICENSE +0 -21
  80. package/skills/implement/SKILL.md +0 -16
  81. package/skills/implement/agents/openai.yaml +0 -4
  82. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  83. package/skills/improve-codebase-architecture/LICENSE +0 -21
  84. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  85. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  86. package/skills/prototype/LICENSE +0 -21
  87. package/skills/prototype/LOGIC.md +0 -67
  88. package/skills/prototype/SKILL.md +0 -28
  89. package/skills/prototype/UI.md +0 -112
  90. package/skills/prototype/agents/openai.yaml +0 -3
  91. package/skills/research/LICENSE +0 -21
  92. package/skills/research/SKILL.md +0 -14
  93. package/skills/research/agents/openai.yaml +0 -3
  94. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  95. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  96. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  97. package/skills/tdd/LICENSE +0 -21
  98. package/skills/tdd/SKILL.md +0 -40
  99. package/skills/tdd/agents/openai.yaml +0 -3
  100. package/skills/tdd/mocking.md +0 -59
  101. package/skills/tdd/tests.md +0 -77
  102. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  103. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  104. package/skills/teach/LICENSE +0 -21
  105. package/skills/teach/MISSION-FORMAT.md +0 -31
  106. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  107. package/skills/teach/SKILL.md +0 -141
  108. package/skills/teach/agents/openai.yaml +0 -4
  109. package/skills/to-questionnaire/LICENSE +0 -21
  110. package/skills/to-questionnaire/SKILL.md +0 -55
  111. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  112. package/skills/to-spec/LICENSE +0 -21
  113. package/skills/to-spec/SKILL.md +0 -76
  114. package/skills/to-spec/agents/openai.yaml +0 -4
  115. package/skills/to-tickets/LICENSE +0 -21
  116. package/skills/to-tickets/SKILL.md +0 -106
  117. package/skills/to-tickets/agents/openai.yaml +0 -4
  118. package/skills/wait-what/LICENSE +0 -21
  119. package/skills/wait-what/SKILL.md +0 -8
  120. package/skills/wait-what/agents/openai.yaml +0 -4
  121. package/skills/wayfinder/LICENSE +0 -21
  122. package/skills/wayfinder/SKILL.md +0 -129
  123. package/skills/wayfinder/agents/openai.yaml +0 -4
  124. package/skills/writing-for-agents/LICENSE +0 -21
  125. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  126. package/skills/writing-for-agents/SKILL.md +0 -83
  127. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/calls.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 Calls {
8
+ house;
9
+ changed;
10
+ carried = new Map();
11
+ path = join(kitHome(), 'call-results.json');
12
+ results;
13
+ returned = new Set();
14
+ open = 0;
15
+ constructor(house, changed) {
16
+ this.house = house;
17
+ this.changed = changed;
18
+ this.results = new Map(existsSync(this.path) ? Object.entries(JSON.parse(readFileSync(this.path, 'utf8'))) : []);
19
+ }
20
+ keepResult(callId, result) {
21
+ if (this.returned.has(callId))
22
+ return;
23
+ this.results.set(callId, result);
24
+ this.save();
25
+ }
26
+ idle() {
27
+ return this.open === 0;
28
+ }
29
+ carry(callId, work) {
30
+ const known = this.carried.get(callId);
31
+ const result = this.results.get(callId);
32
+ const carried = known ?? (result === undefined ? work() : Promise.resolve({ result }));
33
+ if (known === undefined) {
34
+ this.carried.set(callId, carried);
35
+ carried.catch(() => this.carried.delete(callId));
36
+ }
37
+ this.open++;
38
+ this.changed();
39
+ return carried
40
+ .then(async ({ result, returned }) => {
41
+ try {
42
+ await this.house.deliver(`/kit/inputs/${callId}/ack`, result);
43
+ }
44
+ finally {
45
+ this.returned.add(callId);
46
+ if (this.results.delete(callId))
47
+ this.save();
48
+ }
49
+ if (known === undefined)
50
+ await returned?.();
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.results)), { mode: 0o600 });
61
+ renameSync(temporary, this.path);
62
+ }
63
+ }
package/dist/chatgpt.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { placeAppGit } from './app-git.js';
2
3
  import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
3
4
  import { accepted, DEFAULT_HOUSE } from './login.js';
4
5
  const KEY_SOURCE = 'New key for a script on Connections';
@@ -17,10 +18,15 @@ export async function chatgpt(argv) {
17
18
  }
18
19
  if (values.key === undefined)
19
20
  throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
20
- const house = values.house ?? (await readOwnAgent())?.house ?? DEFAULT_HOUSE;
21
+ const before = await readOwnAgent();
22
+ const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
21
23
  if (!(await accepted(house, values.key)))
22
24
  throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
23
25
  const relay = process.env.HOUSE_KIT_RELAY || undefined;
24
26
  await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
+ if (before !== null && before.credential !== values.key) {
29
+ process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
+ }
25
31
  process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
26
32
  }
package/dist/clis-main.js CHANGED
@@ -1,8 +1,8 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { post } from './api.js';
3
2
  import { CLIS, below, locate, versionOf } from './clis.js';
4
3
  import { readEnrolment } from './home.js';
5
- setGlobalProxyFromEnv();
4
+ import { proxyUnlessRelayed } from './relay.js';
5
+ proxyUnlessRelayed();
6
6
  try {
7
7
  const { house, credential } = (await readEnrolment());
8
8
  const { agents } = await post(house, '/kit/agents/desired', {}, credential);
package/dist/clis.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { constants, readFileSync } from 'node:fs';
3
- import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
6
  import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
+ import { downloadServed, servedDigest } from './building.js';
6
8
  import { kitHome } from './home.js';
9
+ import { childEnvironment } from './relay.js';
7
10
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
8
11
  function houseRule(houses) {
9
12
  return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
@@ -54,18 +57,21 @@ export async function allowHouseInCodex(path) {
54
57
  houses.push(house);
55
58
  }
56
59
  await mkdir(rules, { recursive: true });
57
- const temporary = `${file}.${process.pid}`;
60
+ const temporary = `${file}.${randomUUID()}`;
58
61
  await writeFile(temporary, houseRule(houses));
59
62
  await rename(temporary, file);
60
63
  }
61
64
  async function nothing() { }
65
+ function answered(error) {
66
+ return typeof error === 'object' && error !== null ? error : {};
67
+ }
62
68
  function bridgeInClaudeSandbox(bridge) {
63
69
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
64
70
  }
65
71
  export const CLIS = {
66
72
  'codex-acp': {
67
73
  bin: 'codex',
68
- minimum: '0.159.1',
74
+ minimum: '0.160.1',
69
75
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
70
76
  adapter: {
71
77
  package: '@agentclientprotocol/codex-acp',
@@ -76,6 +82,10 @@ export const CLIS = {
76
82
  args: [],
77
83
  fullAccess: 'agent-full-access',
78
84
  sessionMeta: () => ({}),
85
+ unknownSession: (error) => {
86
+ const { code, data } = answered(error);
87
+ return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
88
+ },
79
89
  renames: true,
80
90
  allowHouse: allowHouseInCodex,
81
91
  signedIn: { command: ['login', 'status'] },
@@ -84,7 +94,7 @@ export const CLIS = {
84
94
  },
85
95
  'claude-agent-acp': {
86
96
  bin: 'claude',
87
- minimum: '2.1.286',
97
+ minimum: '2.1.303',
88
98
  install: 'curl -fsSL https://claude.ai/install.sh | bash',
89
99
  adapter: {
90
100
  package: '@agentclientprotocol/claude-agent-acp',
@@ -105,6 +115,7 @@ export const CLIS = {
105
115
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
106
116
  },
107
117
  }),
118
+ unknownSession: (error) => answered(error).code === -32002,
108
119
  renames: false,
109
120
  allowHouse: nothing,
110
121
  signedIn: { command: ['auth', 'status'] },
@@ -119,6 +130,7 @@ export const CLIS = {
119
130
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
120
131
  fullAccess: null,
121
132
  sessionMeta: () => ({}),
133
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
122
134
  renames: false,
123
135
  allowHouse: nothing,
124
136
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -127,8 +139,7 @@ export const CLIS = {
127
139
  },
128
140
  };
129
141
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
130
- export const KIT_VERSION = KIT_PACKAGE.version;
131
- const PROXY = /^https?_proxy$/i;
142
+ export const KIT_RELEASE = KIT_PACKAGE.version;
132
143
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
133
144
  // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
134
145
  const INSTALL_MS = 15 * 60_000;
@@ -150,15 +161,12 @@ async function installedAdapter(kind) {
150
161
  return null;
151
162
  }
152
163
  }
153
- export function withoutProxy() {
154
- return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
155
- }
156
164
  export function run(command, args, timeoutMs, detached = false, signal) {
157
165
  return new Promise((resolve) => {
158
166
  const child = spawn(command, args, {
159
167
  stdio: ['ignore', 'pipe', 'pipe'],
160
168
  timeout: timeoutMs,
161
- env: withoutProxy(),
169
+ env: childEnvironment(),
162
170
  detached,
163
171
  signal: detached ? undefined : signal,
164
172
  });
@@ -192,15 +200,37 @@ export function run(command, args, timeoutMs, detached = false, signal) {
192
200
  export function failureOf(ran, program) {
193
201
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
194
202
  }
203
+ export function adapterArchive(kind) {
204
+ return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
205
+ }
195
206
  export async function installAdapter(kind, signal) {
196
207
  const adapter = CLIS[kind].adapter;
197
208
  if (adapter === null)
198
209
  return null;
199
- const release = KIT_PACKAGE.peerDependencies[adapter.package];
200
- if ((await installedAdapter(kind)) === release)
210
+ if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
201
211
  return null;
202
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
203
- return ran.status === 0 ? null : failureOf(ran, 'npm');
212
+ const name = adapterArchive(kind);
213
+ const archive = `${prefix(kind)}.tar.gz`;
214
+ const deadline = AbortSignal.timeout(INSTALL_MS);
215
+ const bounded = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
216
+ try {
217
+ const sha256 = await servedDigest(name, bounded);
218
+ if (sha256 === null)
219
+ return `House serves no ${name}`;
220
+ await rm(prefix(kind), { recursive: true, force: true });
221
+ await mkdir(prefix(kind), { recursive: true });
222
+ await downloadServed(name, sha256, archive, bounded);
223
+ const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
224
+ return ran.status === 0 ? null : failureOf(ran, 'tar');
225
+ }
226
+ catch (error) {
227
+ if (deadline.aborted && signal?.aborted !== true)
228
+ return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
229
+ return error instanceof Error ? error.message : String(error);
230
+ }
231
+ finally {
232
+ await rm(archive, { force: true });
233
+ }
204
234
  }
205
235
  export async function installCli(kind, signal) {
206
236
  const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
@@ -215,10 +245,13 @@ const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$H
215
245
  function inLoginShell(script, argument = '', signal) {
216
246
  return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
217
247
  }
218
- export async function locate(kind, signal) {
219
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
248
+ export async function onLoginPath(program, signal) {
249
+ const ran = await inLoginShell(LOOKUP, program, signal);
220
250
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
221
251
  }
252
+ export function locate(kind, signal) {
253
+ return onLoginPath(CLIS[kind].bin, signal);
254
+ }
222
255
  export async function loginPath() {
223
256
  const marker = 'house-kit-path ';
224
257
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
@@ -0,0 +1,126 @@
1
+ import { subscribe } from 'node:diagnostics_channel';
2
+ import { HouseRefusal, retryDelay } from './api.js';
3
+ import { end, REPLACED, signInAgain } from './end.js';
4
+ import { readEnrolment } from './home.js';
5
+ import { relayed } from './relay.js';
6
+ import { endUnserved, KIT_WIRE } from './wire.js';
7
+ const PATH = '/kit/stream';
8
+ const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
9
+ const REFUSAL_HEADER = 'x-house-refusal';
10
+ // House beats each Kit connection every thirty seconds, so two silent beats mean this Kit process cannot hear House.
11
+ export const CONNECTION_SILENCE_MS = 60_000;
12
+ const SYSTEM_CONNECTION_CLOCK = {
13
+ later: (delay, callback) => setTimeout(callback, delay),
14
+ clear: (timer) => clearTimeout(timer),
15
+ };
16
+ export class ConnectionSilence {
17
+ timer = null;
18
+ silent;
19
+ clock;
20
+ interval;
21
+ constructor(silent, clock = SYSTEM_CONNECTION_CLOCK, interval = CONNECTION_SILENCE_MS) {
22
+ this.silent = silent;
23
+ this.clock = clock;
24
+ this.interval = interval;
25
+ }
26
+ heard() {
27
+ this.stop();
28
+ this.timer = this.clock.later(this.interval, () => {
29
+ this.timer = null;
30
+ this.silent();
31
+ });
32
+ }
33
+ stop() {
34
+ if (this.timer === null)
35
+ return;
36
+ this.clock.clear(this.timer);
37
+ this.timer = null;
38
+ }
39
+ }
40
+ function connectionUrl(house) {
41
+ const url = new URL(PATH, house);
42
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
43
+ return url;
44
+ }
45
+ function header(headers, name) {
46
+ for (let index = 0; index < headers.length; index += 2) {
47
+ if (headers[index].toString('latin1').toLowerCase() === name)
48
+ return headers[index + 1].toString('latin1');
49
+ }
50
+ return '';
51
+ }
52
+ let refused = null;
53
+ subscribe('undici:request:headers', (message) => {
54
+ const { request, response } = message;
55
+ if (!request.path.endsWith(PATH))
56
+ return;
57
+ refused = new HouseRefusal(PATH, response.statusCode, header(response.headers, REFUSAL_HEADER) || response.statusText);
58
+ });
59
+ export function holdConnection(handlers) {
60
+ let connection = null;
61
+ let failures = 0;
62
+ const connect = async () => {
63
+ const { house, credential } = (await readEnrolment());
64
+ refused = null;
65
+ const opening = new WebSocket(relayed(connectionUrl(house)), {
66
+ protocols: [SUBPROTOCOL],
67
+ headers: { authorization: `Bearer ${credential}` },
68
+ });
69
+ let opened = false;
70
+ let silence = null;
71
+ const closed = (event) => {
72
+ silence?.stop();
73
+ silence = null;
74
+ if (connection === opening)
75
+ connection = null;
76
+ handlers.closed();
77
+ process.stderr.write(`kit: connection closed ${event.code} ${event.reason}\n`);
78
+ if (event.reason === 'replaced')
79
+ end(REPLACED);
80
+ if (event.reason === 'credential_rejected' || refused?.status === 401) {
81
+ void signInAgain(credential);
82
+ return;
83
+ }
84
+ if (refused !== null)
85
+ endUnserved(refused.text);
86
+ if (!opened && refused !== null)
87
+ process.stderr.write(`kit: ${refused.message}\n`);
88
+ // 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.
89
+ setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
90
+ };
91
+ opening.onopen = () => {
92
+ opened = true;
93
+ failures = 0;
94
+ connection = opening;
95
+ silence = new ConnectionSilence(() => {
96
+ if (connection !== opening)
97
+ return;
98
+ connection = null;
99
+ opening.onclose = null;
100
+ opening.close();
101
+ closed(new CloseEvent('close', { code: 1006, reason: 'heartbeat_missed' }));
102
+ });
103
+ silence.heard();
104
+ handlers.opened();
105
+ };
106
+ opening.onmessage = (event) => {
107
+ silence?.heard();
108
+ const arrived = JSON.parse(String(event.data));
109
+ if (arrived.type === 'input')
110
+ handlers.call(arrived);
111
+ else if (arrived.type !== 'beat')
112
+ handlers.notification(arrived);
113
+ };
114
+ opening.onerror = () => undefined;
115
+ opening.onclose = closed;
116
+ };
117
+ void connect();
118
+ return {
119
+ send(notification) {
120
+ if (connection === null || connection.readyState !== WebSocket.OPEN)
121
+ return false;
122
+ connection.send(JSON.stringify(notification));
123
+ return true;
124
+ },
125
+ };
126
+ }