@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c14f8446ba284

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/resident.js CHANGED
@@ -1,33 +1,32 @@
1
1
  import { once } from 'node:events';
2
+ import { watch } from 'node:fs';
2
3
  import { mkdir, rm, writeFile } from 'node:fs/promises';
3
4
  import { createServer } from 'node:http';
4
- import { extname, join } from 'node:path';
5
+ import { dirname, extname, join } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
- import { killDescendants } from './acp.js';
7
7
  import { Agents } from './agents.js';
8
8
  import { house } from './api.js';
9
9
  import { received } from './bridge.js';
10
10
  import { Conversations } from './conversations.js';
11
- import { WorkingCopies } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { LocalCopies } from './copies.js';
12
+ import { agentBase, enrolled, kitHome } from './home.js';
13
+ import { killDescendants } from './scope.js';
14
+ import { quoted } from './shell.js';
13
15
  import { SignIns } from './sign-in.js';
14
16
  import { placeSkillSet } from './skills.js';
15
17
  import { holdStream } from './stream.js';
18
+ import { Updates } from './update.js';
19
+ import { codexLauncher } from './work.js';
16
20
  function report(error) {
17
21
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
18
22
  }
19
23
  function logged(work) {
20
24
  work.catch(report);
21
25
  }
22
- function quoted(value) {
23
- return `'${value.replaceAll("'", `'\\''`)}'`;
24
- }
25
- async function installShim() {
26
- const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
27
- await mkdir(join(kitHome(), 'shim'), { recursive: true });
28
- await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
29
- mode: 0o755,
30
- });
26
+ async function installShim(path, module) {
27
+ const main = fileURLToPath(new URL(`./${module}${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
28
+ await mkdir(dirname(path), { recursive: true });
29
+ await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
31
30
  }
32
31
  async function serveOwner(copies) {
33
32
  const socketPath = join(kitHome(), 'owner.sock');
@@ -49,18 +48,38 @@ async function serveOwner(copies) {
49
48
  server.listen(socketPath);
50
49
  await once(server, 'listening');
51
50
  }
51
+ function connected() {
52
+ return new Promise((resolve) => {
53
+ const watcher = watch(kitHome(), () => {
54
+ if (!enrolled())
55
+ return;
56
+ watcher.close();
57
+ resolve();
58
+ });
59
+ if (!enrolled()) {
60
+ process.stderr.write('kit: this Environment is not connected; run kit login\n');
61
+ return;
62
+ }
63
+ watcher.close();
64
+ resolve();
65
+ });
66
+ }
52
67
  export async function resident() {
53
- if ((await readEnrolment()) === null) {
54
- process.stderr.write('kit: this Environment is not enrolled; run kit login\n');
55
- process.exitCode = 1;
56
- return;
57
- }
68
+ await connected();
69
+ watch(kitHome(), () => {
70
+ if (enrolled())
71
+ return;
72
+ killDescendants();
73
+ process.exit(1);
74
+ });
58
75
  await placeSkillSet();
76
+ await mkdir(agentBase(), { recursive: true });
59
77
  process.env.HOUSE_KIT_RESIDENT = String(process.pid);
60
78
  const agents = new Agents(house);
61
- const copies = new WorkingCopies(house);
79
+ const copies = new LocalCopies(house);
62
80
  await copies.load();
63
- await installShim();
81
+ await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
82
+ await installShim(codexLauncher(), 'codex-main');
64
83
  await serveOwner(copies);
65
84
  let stream = null;
66
85
  let reported = false;
@@ -82,6 +101,7 @@ export async function resident() {
82
101
  changed,
83
102
  });
84
103
  const signIns = new SignIns(house, agents, changed);
104
+ const updates = new Updates(house);
85
105
  for (const signal of ['SIGTERM', 'SIGINT']) {
86
106
  process.once(signal, () => {
87
107
  killDescendants();
@@ -89,6 +109,7 @@ export async function resident() {
89
109
  });
90
110
  }
91
111
  await house.deliver('/kit/restarted', {}).catch(report);
112
+ await updates.resumed().catch(report);
92
113
  logged(agents.refresh());
93
114
  stream = holdStream({
94
115
  opened: () => {
@@ -97,9 +118,11 @@ export async function resident() {
97
118
  logged(copies.select());
98
119
  },
99
120
  frame: (arrived) => {
121
+ if (arrived.type === 'watching')
122
+ conversations.watching(arrived.watching === true);
100
123
  if (arrived.type === 'work_available' && arrived.subject === 'agents')
101
124
  logged(agents.refresh());
102
- if (arrived.type === 'work_available' && arrived.subject === 'working_copy')
125
+ if (arrived.type === 'work_available' && arrived.subject === 'local_copy')
103
126
  logged(copies.select());
104
127
  if (arrived.type === 'entries') {
105
128
  copies.entries(arrived);
@@ -108,6 +131,8 @@ export async function resident() {
108
131
  reported = false;
109
132
  if (arrived.kind === 'sign_in')
110
133
  signIns.start(arrived);
134
+ else if (arrived.kind === 'update')
135
+ updates.start(arrived);
111
136
  else
112
137
  conversations.input(arrived);
113
138
  }
package/dist/scope.js ADDED
@@ -0,0 +1,203 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const LISTING_BYTES = 1 << 28;
5
+ const CGROUPS = '/sys/fs/cgroup';
6
+ const GONE = new Set(['ENOENT', 'ESRCH']);
7
+ function failure(error) {
8
+ return error.code ?? '';
9
+ }
10
+ function listed(environment) {
11
+ const found = new Map();
12
+ const output = execFileSync('ps', [environment ? '-AEww' : '-Aww', '-o', 'pid=,ppid=,sess=,command='], {
13
+ encoding: 'utf8',
14
+ maxBuffer: LISTING_BYTES,
15
+ });
16
+ for (const line of output.split('\n')) {
17
+ const fields = /^\s*(\d+)\s+(\d+)\s+(\S+)\s?(.*)$/.exec(line);
18
+ if (fields !== null)
19
+ found.set(Number(fields[1]), { parent: Number(fields[2]), session: fields[3], command: fields[4] });
20
+ }
21
+ return found;
22
+ }
23
+ function read(pid) {
24
+ try {
25
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
26
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
27
+ const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
28
+ return { parent: Number(fields[1]), session: fields[3], command };
29
+ }
30
+ catch (error) {
31
+ if (GONE.has(failure(error)))
32
+ return null;
33
+ throw error;
34
+ }
35
+ }
36
+ function processes() {
37
+ if (process.platform === 'darwin')
38
+ return listed(false);
39
+ const found = new Map();
40
+ for (const entry of readdirSync('/proc')) {
41
+ const running = /^\d+$/.test(entry) ? read(entry) : null;
42
+ if (running !== null)
43
+ found.set(Number(entry), running);
44
+ }
45
+ return found;
46
+ }
47
+ function descendants(roots, table = processes()) {
48
+ const children = new Map();
49
+ for (const [pid, { parent }] of table)
50
+ children.set(parent, [...(children.get(parent) ?? []), pid]);
51
+ const tree = [];
52
+ const pending = roots.flatMap((root) => children.get(root) ?? []);
53
+ while (pending.length > 0) {
54
+ const pid = pending.pop();
55
+ tree.push(pid);
56
+ pending.push(...(children.get(pid) ?? []));
57
+ }
58
+ return tree;
59
+ }
60
+ function marked(marker, doubted) {
61
+ if (process.platform === 'darwin') {
62
+ return [...listed(true)].filter(([, { command }]) => ` ${command} `.includes(` ${marker} `)).map(([pid]) => pid);
63
+ }
64
+ return readdirSync('/proc')
65
+ .filter((entry) => /^\d+$/.test(entry) && carries(entry, marker, doubted))
66
+ .map(Number);
67
+ }
68
+ function carries(pid, marker, doubted) {
69
+ try {
70
+ return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').includes(marker);
71
+ }
72
+ catch (error) {
73
+ if (GONE.has(failure(error)))
74
+ return false;
75
+ if (failure(error) !== 'EACCES')
76
+ throw error;
77
+ return doubted && statSync(`/proc/${pid}`, { throwIfNoEntry: false })?.uid === process.getuid();
78
+ }
79
+ }
80
+ function killAll(pids) {
81
+ for (const pid of pids) {
82
+ for (const target of [-pid, pid]) {
83
+ try {
84
+ process.kill(target, 'SIGKILL');
85
+ }
86
+ catch { }
87
+ }
88
+ }
89
+ }
90
+ export function killTree(child) {
91
+ killAll([child.pid, ...descendants([child.pid])]);
92
+ }
93
+ export function killMarked(marker) {
94
+ const pids = marked(marker, false);
95
+ killAll([...pids, ...descendants(pids)]);
96
+ }
97
+ export function killDescendants() {
98
+ const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`, false);
99
+ killAll([...pids, ...descendants([process.pid, ...pids])]);
100
+ }
101
+ function ownGroup() {
102
+ if (process.platform !== 'linux')
103
+ return null;
104
+ try {
105
+ const line = readFileSync('/proc/self/cgroup', 'utf8')
106
+ .split('\n')
107
+ .find((entry) => entry.startsWith('0::'));
108
+ return line === undefined ? null : join(CGROUPS, line.slice(3));
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ function members(group) {
115
+ const pids = readFileSync(join(group, 'cgroup.procs'), 'utf8').split('\n').filter(Boolean).map(Number);
116
+ for (const entry of readdirSync(group, { withFileTypes: true })) {
117
+ if (entry.isDirectory())
118
+ pids.push(...members(join(group, entry.name)));
119
+ }
120
+ return pids;
121
+ }
122
+ export class Scope {
123
+ marker;
124
+ cli;
125
+ group;
126
+ session = null;
127
+ recorded = null;
128
+ constructor(marker, name, cli) {
129
+ this.marker = marker;
130
+ this.cli = cli;
131
+ const own = ownGroup();
132
+ this.group = own === null ? null : join(own, name);
133
+ try {
134
+ if (this.group !== null)
135
+ mkdirSync(this.group);
136
+ }
137
+ catch {
138
+ this.group = null;
139
+ }
140
+ }
141
+ command(command, args) {
142
+ if (this.group === null)
143
+ return [command, args];
144
+ return ['/bin/sh', ['-c', 'echo $$ > "$0/cgroup.procs" 2>/dev/null; exec "$@"', this.group, command, ...args]];
145
+ }
146
+ entered(pid) {
147
+ this.session = process.platform === 'darwin' ? (listed(false).get(pid)?.session ?? null) : String(pid);
148
+ if (this.group === null)
149
+ return;
150
+ const joined = readFileSync(`/proc/${pid}/cgroup`, 'utf8').split('\n').find((entry) => entry.startsWith('0::'));
151
+ if (joined !== undefined && join(CGROUPS, joined.slice(3)) === this.group)
152
+ return;
153
+ this.close();
154
+ this.group = null;
155
+ }
156
+ record() {
157
+ const found = this.found();
158
+ if (found === null)
159
+ return;
160
+ this.recorded = new Set([...found].filter(([, { session }]) => session === this.session).map(([pid]) => pid));
161
+ }
162
+ working() {
163
+ const found = this.found();
164
+ if (found === null)
165
+ return true;
166
+ if (this.recorded === null)
167
+ return found.size > 0;
168
+ return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
169
+ }
170
+ empty() {
171
+ return this.found()?.size === 0;
172
+ }
173
+ kill() {
174
+ killMarked(this.marker);
175
+ if (this.group === null)
176
+ return;
177
+ try {
178
+ writeFileSync(join(this.group, 'cgroup.kill'), '1');
179
+ }
180
+ catch {
181
+ killAll([...(this.found()?.keys() ?? [])]);
182
+ }
183
+ }
184
+ close() {
185
+ if (this.group === null)
186
+ return;
187
+ try {
188
+ rmdirSync(this.group);
189
+ }
190
+ catch { }
191
+ }
192
+ found() {
193
+ try {
194
+ const table = processes();
195
+ const roots = this.group === null ? marked(this.marker, true) : members(this.group);
196
+ const pids = this.group === null ? [...roots, ...descendants(roots, table)] : roots;
197
+ return new Map(pids.flatMap((pid) => (table.has(pid) ? [[pid, table.get(pid)]] : [])));
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ }
@@ -1,15 +1,20 @@
1
+ import { finalRefusal, retryDelay } from './api.js';
1
2
  export async function holdSecretInput(house, subject, steps, waiting) {
2
- while (waiting()) {
3
+ for (let failures = 0; waiting();) {
3
4
  try {
4
5
  const held = await house.post(`/kit/secret-input/${subject}`, { steps });
5
6
  if (held.outcome === 'collected')
6
7
  return held.content;
7
8
  if (held.release !== 'held')
8
9
  return null;
10
+ failures = 0;
9
11
  }
10
12
  catch (error) {
13
+ if (finalRefusal(error))
14
+ return null;
11
15
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
12
- await new Promise((resolve) => setTimeout(resolve, 1000));
16
+ // A wait that did not reach House is held again on the jittered curve for as long as its asker still waits.
17
+ await new Promise((resolve) => setTimeout(resolve, retryDelay(failures++)));
13
18
  }
14
19
  }
15
20
  return null;
package/dist/shell.js ADDED
@@ -0,0 +1,3 @@
1
+ export function quoted(value) {
2
+ return `'${value.replaceAll("'", `'\\''`)}'`;
3
+ }
package/dist/sign-in.js CHANGED
@@ -1,13 +1,19 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { killTree } from './acp.js';
4
- import { CLIS, cliCommand, withoutProxy } from './clis.js';
3
+ import { CLIS, withoutProxy } from './clis.js';
4
+ import { killTree } from './scope.js';
5
5
  import { holdSecretInput } from './secret-input.js';
6
6
  const ESCAPE = /\u001b\[[\d;]*m/g;
7
7
  const LINK = /https:\/\/\S+/;
8
8
  const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
9
+ function causeOf(error) {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
9
12
  function logged(error) {
10
- process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
13
+ process.stderr.write(`kit: ${causeOf(error)}\n`);
14
+ }
15
+ function endOf(signal) {
16
+ return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
11
17
  }
12
18
  export class SignIns {
13
19
  house;
@@ -27,24 +33,52 @@ export class SignIns {
27
33
  return;
28
34
  this.running.add(input.input_id);
29
35
  this.changed();
30
- this.login(input)
31
- .then(() => this.agents.report())
32
- .then(() => this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {}))
36
+ this.signIn(input)
33
37
  .catch(logged)
34
38
  .finally(() => {
35
39
  this.running.delete(input.input_id);
36
40
  this.changed();
37
41
  });
38
42
  }
39
- async login(input) {
40
- await this.agents.installed();
43
+ async signIn(input) {
44
+ const ending = new AbortController();
45
+ // House stops waiting for a sign-in ten minutes after it wrote it and sends the seconds left, so its install and login end with the page.
46
+ const timer = setTimeout(() => ending.abort(), input.ends_in * 1000);
47
+ const ended = await Promise.race([this.signedIn(input, ending.signal), endOf(ending.signal)]);
48
+ clearTimeout(timer);
49
+ if (ended)
50
+ 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);
54
+ }
55
+ async signedIn(input, signal) {
56
+ await this.login(input, signal);
57
+ if (!signal.aborted)
58
+ await this.agents.recheck(input.cli, signal);
59
+ return false;
60
+ }
61
+ async login(input, signal) {
62
+ let cli;
63
+ try {
64
+ cli = await this.agents.located(input.cli, signal);
65
+ }
66
+ catch (error) {
67
+ if (!signal.aborted)
68
+ process.stderr.write(`kit: the ${input.cli} sign-in failed: ${causeOf(error)}\n`);
69
+ return;
70
+ }
71
+ if (signal.aborted)
72
+ return;
41
73
  const login = CLIS[input.cli].login;
42
- const child = spawn(cliCommand(input.cli), login.args, {
74
+ const child = spawn(cli, login.args, {
43
75
  stdio: ['pipe', 'pipe', 'pipe'],
44
76
  detached: true,
45
77
  env: withoutProxy(),
46
78
  });
47
79
  const running = () => child.exitCode === null && child.signalCode === null;
80
+ const stop = () => killTree(child);
81
+ signal.addEventListener('abort', stop, { once: true });
48
82
  let link;
49
83
  let code;
50
84
  let last = '';
@@ -54,6 +88,8 @@ export class SignIns {
54
88
  if (plain === '')
55
89
  return;
56
90
  last = plain;
91
+ if (login.code === 'collect' && plain.includes(login.rejected))
92
+ killTree(child);
57
93
  link ??= LINK.exec(plain)?.[0];
58
94
  if (CODE.test(plain))
59
95
  code ??= plain;
@@ -79,9 +115,10 @@ export class SignIns {
79
115
  createInterface({ input: output }).on('line', read);
80
116
  const failed = await new Promise((resolve) => {
81
117
  child.on('error', (error) => resolve(error.message));
82
- child.on('close', (status, signal) => resolve(status === 0 ? null : `exited with ${status ?? signal}${last ? `: ${last}` : ''}`));
118
+ child.on('close', (status, killed) => resolve(status === 0 ? null : `exited with ${status ?? killed}${last ? `: ${last}` : ''}`));
83
119
  });
84
- if (failed !== null)
120
+ signal.removeEventListener('abort', stop);
121
+ if (failed !== null && !signal.aborted)
85
122
  process.stderr.write(`kit: the ${input.cli} sign-in failed: ${failed}\n`);
86
123
  }
87
124
  }
package/dist/skills.js CHANGED
@@ -3,11 +3,10 @@ import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'nod
3
3
  import { homedir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { parse } from 'yaml';
7
6
  import { KIT_VERSION } from './clis.js';
8
- import { refusalCode } from './copies.js';
9
7
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
10
- export const HOW_WE_WORK = '/private/library/how-we-work.md';
8
+ import { refusalOf } from './refusals.js';
9
+ export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
11
10
  const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
12
11
  const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
13
12
  const RECORD = 'skill-set.json';
@@ -63,20 +62,16 @@ export async function placeSkillSet() {
63
62
  else
64
63
  await rm(join(kitHome(), RECORD), { force: true });
65
64
  }
66
- function documentPresent(refusal) {
67
- const { conflicts } = parse(refusal.slice(refusal.indexOf('\n') + 1));
68
- return conflicts.some((conflict) => conflict.code === 'path_exists');
69
- }
70
65
  export async function createHowWeWork(bridge) {
71
66
  if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
72
67
  return;
73
68
  const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
74
69
  if (created.isError === true) {
75
70
  const text = created.content.map((content) => content.text).join('\n');
76
- const code = refusalCode(text);
77
- if (UNWRITABLE.has(code))
71
+ const refusal = refusalOf(text);
72
+ if (UNWRITABLE.has(refusal?.code ?? ''))
78
73
  return;
79
- if (code !== 'edit_conflict' || !documentPresent(text))
74
+ if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
80
75
  throw new Error(text);
81
76
  }
82
77
  await writeFile(join(kitHome(), CREATED), '');
package/dist/stream.js CHANGED
@@ -1,16 +1,37 @@
1
+ import { subscribe } from 'node:diagnostics_channel';
2
+ import { finalRefusal, HouseRefusal, retryDelay } from './api.js';
3
+ import { end, REPLACED, SIGN_IN_AGAIN } from './end.js';
1
4
  import { readEnrolment } from './home.js';
5
+ import { relayed } from './relay.js';
6
+ const PATH = '/kit/stream';
2
7
  const SUBPROTOCOL = 'house.kit.stream.1';
8
+ const REFUSAL_HEADER = 'x-house-refusal';
3
9
  function streamUrl(house) {
4
- const url = new URL('/kit/stream', house);
10
+ const url = new URL(PATH, house);
5
11
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
6
12
  return url;
7
13
  }
14
+ function header(headers, name) {
15
+ for (let index = 0; index < headers.length; index += 2) {
16
+ if (headers[index].toString('latin1').toLowerCase() === name)
17
+ return headers[index + 1].toString('latin1');
18
+ }
19
+ return '';
20
+ }
21
+ let refused = null;
22
+ subscribe('undici:request:headers', (message) => {
23
+ const { request, response } = message;
24
+ if (!request.path.endsWith(PATH))
25
+ return;
26
+ refused = new HouseRefusal(PATH, response.statusCode, header(response.headers, REFUSAL_HEADER) || response.statusText);
27
+ });
8
28
  export function holdStream(handlers) {
9
29
  let socket = null;
10
30
  let failures = 0;
11
31
  const connect = async () => {
12
32
  const { house, credential } = (await readEnrolment());
13
- const opening = new WebSocket(streamUrl(house), {
33
+ refused = null;
34
+ const opening = new WebSocket(relayed(streamUrl(house)), {
14
35
  protocols: [SUBPROTOCOL],
15
36
  headers: { authorization: `Bearer ${credential}` },
16
37
  });
@@ -29,10 +50,14 @@ export function holdStream(handlers) {
29
50
  if (socket === opening)
30
51
  socket = null;
31
52
  process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
32
- const delay = opened
33
- ? 1000 + Math.random() * 2000
34
- : Math.min(30_000, 1000 * 2 ** failures++);
35
- setTimeout(() => void connect(), delay);
53
+ if (event.reason === 'replaced')
54
+ end(REPLACED);
55
+ if (event.reason === 'credential_rejected' || refused?.status === 401)
56
+ end(SIGN_IN_AGAIN);
57
+ if (finalRefusal(refused))
58
+ end(refused.message);
59
+ // 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.
60
+ setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
36
61
  };
37
62
  };
38
63
  void connect();