@zgeoff/atc 2.33.0 → 2.35.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/atc",
3
- "version": "2.33.0",
3
+ "version": "2.35.0",
4
4
  "description": "Terminal control tower for coding-agent sessions",
5
5
  "homepage": "https://github.com/zgeoff/atc#readme",
6
6
  "bugs": "https://github.com/zgeoff/atc/issues",
@@ -22,12 +22,15 @@ export interface SpawnOptions {
22
22
 
23
23
  /**
24
24
  * The per-launch choices a spawn asks for. Model and effort are checked
25
- * against what the agent advertises; workspace trust requires a verified clone.
25
+ * against what the agent advertises; workspace trust requires a verified
26
+ * clone; resuming an interrupted turn overrides the daemon's config for the
27
+ * session's whole life.
26
28
  */
27
29
  export interface SpawnOverrides {
28
30
  readonly trustClonedWorkspace?: boolean;
29
31
  readonly model?: string;
30
32
  readonly effort?: string;
33
+ readonly resumeInterruptedTurns?: boolean;
31
34
  }
32
35
 
33
36
  export interface SpawnPlan {
@@ -62,6 +65,15 @@ export interface AuthSelection {
62
65
  readonly brokerRequired: boolean;
63
66
  }
64
67
 
68
+ /**
69
+ * One file a guest plan ships: its text, its bytes, or its bytes with the
70
+ * permission bits it unpacks with.
71
+ */
72
+ export type GuestFile =
73
+ | string
74
+ | Uint8Array
75
+ | { readonly content: Uint8Array; readonly mode: number };
76
+
65
77
  /**
66
78
  * A spawn on a remote host, with the files the harness reads there, keyed
67
79
  * by their path inside the session's guest folder, and the variables the
@@ -69,7 +81,7 @@ export interface AuthSelection {
69
81
  * overrides.
70
82
  */
71
83
  export interface GuestSpawnPlan extends SpawnPlan {
72
- readonly files: Readonly<Record<string, string>>;
84
+ readonly files: Readonly<Record<string, GuestFile>>;
73
85
  readonly env?: Readonly<Record<string, string>>;
74
86
  }
75
87
 
@@ -0,0 +1,91 @@
1
+ import { isRecord } from '../shared/report';
2
+
3
+ /**
4
+ * The user settings a session's own Claude config folder starts with, from
5
+ * the host's own `settings.json`: the keys on the allow-list alone, an
6
+ * `env` block cut to its own allow-list, and a permission default of auto
7
+ * mode, which Claude Code honours at user scope only. Any other key stays on
8
+ * the host, so a setting that holds a credential, a plugin, or a host path
9
+ * never reaches a guest unless it is listed here. The statusline command
10
+ * reads the guest's copy of a script under the host's config folder: a path
11
+ * under any of `hostDirs`, the spellings of that folder, moves under
12
+ * `guestDir`.
13
+ */
14
+ export function buildClaudeBundleSettings(
15
+ settings: unknown,
16
+ hostDirs: readonly string[],
17
+ guestDir: string,
18
+ ): Record<string, unknown> {
19
+ const source = isRecord(settings) ? settings : {};
20
+ const shipped: Record<string, unknown> = {};
21
+
22
+ for (const key of SETTINGS_ALLOW_LIST) {
23
+ if (source[key] !== undefined) {
24
+ shipped[key] = source[key];
25
+ }
26
+ }
27
+
28
+ const env = buildBundleEnv(source['env']);
29
+ const statusLine = buildBundleStatusLine(source['statusLine'], hostDirs, guestDir);
30
+
31
+ return {
32
+ ...shipped,
33
+ ...(env === null ? {} : { env }),
34
+ ...(statusLine === null ? {} : { statusLine }),
35
+ permissions: {
36
+ ...(isRecord(source['permissions']) ? source['permissions'] : {}),
37
+ defaultMode: 'auto',
38
+ },
39
+ };
40
+ }
41
+
42
+ const SETTINGS_ALLOW_LIST = [
43
+ 'model',
44
+ 'effortLevel',
45
+ 'advisorModel',
46
+ 'outputStyle',
47
+ 'autoCompactWindow',
48
+ 'autoMode',
49
+ 'attribution',
50
+ 'includeCoAuthoredBy',
51
+ 'skipAutoPermissionPrompt',
52
+ 'skipWorkflowUsageWarning',
53
+ 'editorMode',
54
+ 'tui',
55
+ ] as const;
56
+
57
+ // The variables an `env` block may ship. A variable off this list may hold
58
+ // a credential, so it stays on the host.
59
+ const ENV_ALLOW_LIST: ReadonlySet<string> = new Set(['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS']);
60
+
61
+ function buildBundleEnv(env: unknown): Record<string, string> | null {
62
+ if (!isRecord(env)) {
63
+ return null;
64
+ }
65
+
66
+ const shipped = Object.fromEntries(
67
+ Object.entries(env).filter(
68
+ (entry): entry is [string, string] =>
69
+ ENV_ALLOW_LIST.has(entry[0]) && typeof entry[1] === 'string',
70
+ ),
71
+ );
72
+
73
+ return Object.keys(shipped).length === 0 ? null : shipped;
74
+ }
75
+
76
+ function buildBundleStatusLine(
77
+ statusLine: unknown,
78
+ hostDirs: readonly string[],
79
+ guestDir: string,
80
+ ): Record<string, unknown> | null {
81
+ if (!isRecord(statusLine) || typeof statusLine['command'] !== 'string') {
82
+ return null;
83
+ }
84
+
85
+ const command = hostDirs.reduce(
86
+ (rewritten, hostDir) => rewritten.replaceAll(`${hostDir}/`, `${guestDir}/`),
87
+ statusLine['command'],
88
+ );
89
+
90
+ return { ...statusLine, command };
91
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SpawnPlan } from './agent-adapter';
2
+ import { CLAUDE_CONFIG_BUNDLE_FOLDER } from './claude-config-bundle-folder';
2
3
  import { CLAUDE_CONFIG_SEED_FILE } from './claude-config-seed-file';
3
4
 
4
5
  /**
@@ -7,7 +8,11 @@ import { CLAUDE_CONFIG_SEED_FILE } from './claude-config-seed-file';
7
8
  * setting of the host's image reaches it. A shell copies the seed into the
8
9
  * folder only when the folder holds no `.claude.json` yet, then runs the
9
10
  * CLI, so a resumed session keeps the state the CLI wrote, a folder trust
10
- * a person accepted included. `argv` is the CLI's own command line. The
11
+ * a person accepted included. When the guest folder holds the config bundle
12
+ * staged under `bundleKey`, the shell replaces the bundle's entries in the
13
+ * config folder with the staged ones and removes every staged bundle, so
14
+ * each launch reads the bundle its own transfer carried and never one an
15
+ * earlier launch left. `argv` is the CLI's own command line. The
11
16
  * shell exits before it runs the CLI when the host's environment sets any
12
17
  * of `refusedEnv`, which must be variable names, and says which one.
13
18
  */
@@ -15,7 +20,8 @@ export function buildClaudeGuestLaunch(
15
20
  guestDir: string,
16
21
  argv: readonly string[],
17
22
  refusedEnv: readonly string[] = [],
18
- ): SpawnPlan & { readonly env: Readonly<Record<string, string>> } {
23
+ bundleKey = 'none',
24
+ ): SpawnPlan & { readonly env: { readonly CLAUDE_CONFIG_DIR: string } } {
19
25
  const configDir = `${guestDir}/${CLAUDE_CONFIG_FOLDER}`;
20
26
  const guards = refusedEnv.map((name) => buildEnvGuard(name)).join('');
21
27
 
@@ -27,6 +33,8 @@ export function buildClaudeGuestLaunch(
27
33
  'sh',
28
34
  configDir,
29
35
  `${guestDir}/${CLAUDE_CONFIG_SEED_FILE}`,
36
+ `${guestDir}/${CLAUDE_CONFIG_BUNDLE_FOLDER}`,
37
+ `${guestDir}/${CLAUDE_CONFIG_BUNDLE_FOLDER}/${bundleKey}`,
30
38
  ...argv,
31
39
  ],
32
40
  env: { CLAUDE_CONFIG_DIR: configDir },
@@ -49,7 +57,14 @@ function buildEnvGuard(name: string): string {
49
57
  const REFUSED_EXIT = 78;
50
58
  const CLAUDE_CONFIG_FOLDER = 'claude-config';
51
59
 
52
- // The arguments are the config folder, the seed, and the CLI's own command
53
- // line.
54
- const SEED_CONFIG_SCRIPT =
55
- 'mkdir -p "$1" && { [ -e "$1/.claude.json" ] || cp "$2" "$1/.claude.json"; } && shift 2 && exec "$@"';
60
+ // The arguments are the config folder, the seed, the folder bundles stage
61
+ // in, this launch's staged bundle, and the CLI's own command line. The
62
+ // removed entries are every top-level entry a bundle can hold, so one the
63
+ // host dropped leaves the config folder too.
64
+ const SEED_CONFIG_SCRIPT = [
65
+ 'mkdir -p "$1"',
66
+ '{ [ -e "$1/.claude.json" ] || cp "$2" "$1/.claude.json"; }',
67
+ '{ [ ! -d "$4" ] || { rm -rf "$1/CLAUDE.md" "$1/settings.json" "$1/statusline.sh" "$1/agents" "$1/output-styles" "$1/skills" && cp -R "$4/." "$1/" && rm -rf "$3"; }; }',
68
+ 'shift 4',
69
+ 'exec "$@"',
70
+ ].join(' && ');
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { z } from 'zod';
3
4
  import type { AdapterEvent } from '../protocol/adapter-event';
@@ -30,13 +31,16 @@ import { buildClaudeConfigSeed } from './build-claude-config-seed';
30
31
  import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
31
32
  import { buildClaudeOverrideArgs } from './build-claude-override-args';
32
33
  import { buildHookSettings } from './build-hook-settings';
34
+ import { CLAUDE_CONFIG_BUNDLE_FOLDER } from './claude-config-bundle-folder';
33
35
  import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
34
36
  import { findClaudePermissionMode } from './find-claude-permission-mode';
35
37
  import { findFlagValue } from './find-flag-value';
38
+ import { loadClaudeConfigBundle } from './load-claude-config-bundle';
36
39
  import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
37
40
  import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
38
41
  import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
39
42
  import { planPastedLineInput } from './plan-pasted-line-input';
43
+ import { resolveAgentHome } from './resolve-agent-home';
40
44
  import { resolveClaudeGlobalConfigPath } from './resolve-claude-global-config-path';
41
45
  import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
42
46
  import { updateClaudeProjectTrust } from './update-claude-project-trust';
@@ -189,9 +193,11 @@ export class ClaudeAdapter implements AgentAdapter {
189
193
  // A settings file of the session's own per binding revision carries the
190
194
  // placeholder, and so does the CLI's environment. The configured
191
195
  // arguments go without a permission mode, so the mode the session's own
192
- // user settings set applies. A credential, endpoint, or provider variable
193
- // in the configured settings, or in the host's environment, would keep
194
- // the CLI from sending the placeholder, so either refuses the start.
196
+ // user settings set applies. Those user settings, with the rest of the
197
+ // config bundle, come from the host's own Claude config folder as it is
198
+ // at this launch, staged under a key of this launch's own. A credential, endpoint, or provider variable in the
199
+ // configured settings, or in the host's environment, would keep the CLI
200
+ // from sending the placeholder, so either refuses the start.
195
201
  private planSubscriptionGuestSpawn(
196
202
  opts: SpawnOptions,
197
203
  dir: string,
@@ -206,12 +212,7 @@ export class ClaudeAdapter implements AgentAdapter {
206
212
  }
207
213
 
208
214
  const settingsPath = `auth-r${auth.revision}/settings.json`;
209
-
210
- const settings = buildHookSettings(
211
- { id: this.id, env: { ...auth.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' } },
212
- 0,
213
- argv,
214
- );
215
+ const bundleKey = randomUUID();
215
216
 
216
217
  const launch = buildClaudeGuestLaunch(
217
218
  dir,
@@ -225,6 +226,21 @@ export class ClaudeAdapter implements AgentAdapter {
225
226
  ),
226
227
  ],
227
228
  [...OUTRANKING_VARIABLES],
229
+ bundleKey,
230
+ );
231
+
232
+ const bundle = loadClaudeConfigBundle(
233
+ resolveAgentHome('CLAUDE_CONFIG_DIR', '.claude'),
234
+ launch.env.CLAUDE_CONFIG_DIR,
235
+ );
236
+
237
+ const userSettings = bundle['settings.json'];
238
+ const padding = typeof userSettings === 'string' ? findStatuslinePadding(userSettings) : 0;
239
+
240
+ const settings = buildHookSettings(
241
+ { id: this.id, env: { ...auth.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' } },
242
+ padding,
243
+ argv,
228
244
  );
229
245
 
230
246
  return {
@@ -233,6 +249,12 @@ export class ClaudeAdapter implements AgentAdapter {
233
249
  files: {
234
250
  [settingsPath]: JSON.stringify(settings, null, 2),
235
251
  ...buildClaudeConfigSeed(null),
252
+ ...Object.fromEntries(
253
+ Object.entries(bundle).map(([path, content]) => [
254
+ `${CLAUDE_CONFIG_BUNDLE_FOLDER}/${bundleKey}/${path}`,
255
+ content,
256
+ ]),
257
+ ),
236
258
  ...Object.fromEntries(bridge),
237
259
  },
238
260
  env: { ...launch.env, ...auth.env },
@@ -464,6 +486,20 @@ const OUTRANKING_VARIABLES: ReadonlySet<string> = new Set([
464
486
  'CLAUDE_CODE_USE_GATEWAY',
465
487
  ]);
466
488
 
489
+ // The padding of the statusline the bundle's user settings set, which atc's
490
+ // own statusline mirrors since it renders that one first, or none.
491
+ function findStatuslinePadding(settings: string): number {
492
+ try {
493
+ const parsed: unknown = JSON.parse(settings);
494
+ const statusLine = isRecord(parsed) ? parsed['statusLine'] : undefined;
495
+ const padding = isRecord(statusLine) ? statusLine['padding'] : undefined;
496
+
497
+ return typeof padding === 'number' ? padding : 0;
498
+ } catch {
499
+ return 0;
500
+ }
501
+ }
502
+
467
503
  // The aliases Claude Code documents for `--model`, each resolving to a model
468
504
  // the account picks. A full model name is accepted as well.
469
505
  const CLAUDE_MODEL_ALIASES = [
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Where the Claude config bundle of a session travels inside its guest
3
+ * folder, each launch's bundle under a key of its own: beside the config
4
+ * folder rather than in it, so the launch replaces the bundle's own entries
5
+ * in the config folder whole, and an entry the host no longer holds leaves
6
+ * the config folder with it.
7
+ */
8
+ export const CLAUDE_CONFIG_BUNDLE_FOLDER = 'claude-config-bundle';
@@ -0,0 +1,169 @@
1
+ import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
2
+ import type { Stats } from 'node:fs';
3
+ import { basename, join, relative, sep } from 'node:path';
4
+ import { resolveHomeDir } from '../shared/resolve-home-dir';
5
+ import type { GuestFile } from './agent-adapter';
6
+ import { buildClaudeBundleSettings } from './build-claude-bundle-settings';
7
+
8
+ /**
9
+ * The files of the Claude config bundle, read from the host's config folder
10
+ * `hostDir` and keyed by their path inside the guest's config folder
11
+ * `guestDir`: `CLAUDE.md`, the filtered `settings.json`, `statusline.sh`,
12
+ * every file under `agents/` and `output-styles/`, and each folder under
13
+ * `skills/` that holds a `SKILL.md`. Symlinks resolve to copies of what
14
+ * they point at, and a file its owner may execute unpacks executable.
15
+ * Nothing else in the host's folder ships, so its account state, history,
16
+ * credentials, and backups stay there, and so does any entry whose name
17
+ * starts with a dot. A symlink ships only when what it resolves to could
18
+ * ship by its own path: a file whose name starts with a dot never does, and
19
+ * neither does a file in the host's folder outside the shipped entries. A
20
+ * missing entry ships nothing.
21
+ */
22
+ export function loadClaudeConfigBundle(
23
+ hostDir: string,
24
+ guestDir: string,
25
+ ): Record<string, GuestFile> {
26
+ const hostSettings = readSettings(join(hostDir, 'settings.json'));
27
+
28
+ const settings = buildClaudeBundleSettings(
29
+ hostSettings,
30
+ buildHostDirSpellings(hostDir),
31
+ guestDir,
32
+ );
33
+
34
+ const files: Record<string, GuestFile> = {
35
+ 'settings.json': JSON.stringify(settings, null, 2),
36
+ };
37
+
38
+ const hostReal = findStats(hostDir) === null ? hostDir : realpathSync(hostDir);
39
+
40
+ for (const name of ['CLAUDE.md', 'statusline.sh']) {
41
+ const path = join(hostDir, name);
42
+ const stats = findStats(path);
43
+
44
+ if (stats?.isFile() === true && shouldShipFile(path, hostReal)) {
45
+ files[name] = readBundleFile(path, stats.mode);
46
+ }
47
+ }
48
+
49
+ for (const name of ['agents', 'output-styles']) {
50
+ Object.assign(files, collectTreeFiles(join(hostDir, name), name, hostReal, new Set()));
51
+ }
52
+
53
+ const skills = join(hostDir, 'skills');
54
+
55
+ for (const name of readEntryNames(skills)) {
56
+ const path = join(skills, name);
57
+
58
+ if (findStats(join(path, 'SKILL.md'))?.isFile() === true) {
59
+ Object.assign(files, collectTreeFiles(path, `skills/${name}`, hostReal, new Set()));
60
+ }
61
+ }
62
+
63
+ return files;
64
+ }
65
+
66
+ // The ways a command can spell the host's config folder: its path, and
67
+ // when it is the default folder in the user's home, the home-relative forms
68
+ // a shell expands.
69
+ function buildHostDirSpellings(hostDir: string): string[] {
70
+ return hostDir === join(resolveHomeDir(), '.claude')
71
+ ? [hostDir, '~/.claude', '$HOME/.claude']
72
+ : [hostDir];
73
+ }
74
+
75
+ // The host's settings, or none when the file is missing or is not JSON,
76
+ // which still ships the auto-mode default.
77
+ function readSettings(path: string): unknown {
78
+ try {
79
+ return JSON.parse(readFileSync(path, 'utf8'));
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ // The stats of what a path resolves to, or null for a missing path or a
86
+ // symlink that points nowhere.
87
+ function findStats(path: string): Stats | null {
88
+ try {
89
+ return statSync(path);
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ // A folder's entry names, sorted, without the ones that start with a dot,
96
+ // or none when the path is not a folder.
97
+ function readEntryNames(dir: string): string[] {
98
+ if (findStats(dir)?.isDirectory() !== true) {
99
+ return [];
100
+ }
101
+
102
+ return readdirSync(dir)
103
+ .filter((name) => !name.startsWith('.'))
104
+ .toSorted();
105
+ }
106
+
107
+ // Every file under `dir` that may ship, keyed by its path under `prefix`,
108
+ // with symlinks followed. `seen` holds the real paths of the folders on the
109
+ // way down, so a symlink back up the tree ends there.
110
+ function collectTreeFiles(
111
+ dir: string,
112
+ prefix: string,
113
+ hostReal: string,
114
+ seen: ReadonlySet<string>,
115
+ ): Record<string, GuestFile> {
116
+ const real = findStats(dir) === null ? null : realpathSync(dir);
117
+
118
+ if (real === null || seen.has(real)) {
119
+ return {};
120
+ }
121
+
122
+ const below = new Set([...seen, real]);
123
+
124
+ const files: Record<string, GuestFile> = {};
125
+
126
+ for (const name of readEntryNames(dir)) {
127
+ const path = join(dir, name);
128
+ const stats = findStats(path);
129
+
130
+ if (stats?.isFile() === true && shouldShipFile(path, hostReal)) {
131
+ files[`${prefix}/${name}`] = readBundleFile(path, stats.mode);
132
+ } else if (stats?.isDirectory() === true) {
133
+ Object.assign(files, collectTreeFiles(path, `${prefix}/${name}`, hostReal, below));
134
+ }
135
+ }
136
+
137
+ return files;
138
+ }
139
+
140
+ // Whether the file a path resolves to may ship: its name starts with no
141
+ // dot, and inside the host's real folder `hostReal` it sits in a shipped
142
+ // entry, so a symlink never carries credentials, account state, or the
143
+ // unfiltered settings.
144
+ function shouldShipFile(path: string, hostReal: string): boolean {
145
+ const real = realpathSync(path);
146
+
147
+ if (basename(real).startsWith('.')) {
148
+ return false;
149
+ }
150
+
151
+ const inside = relative(hostReal, real);
152
+
153
+ return inside.startsWith('..') || SHIPPED_ENTRIES.has(inside.split(sep)[0] ?? '');
154
+ }
155
+
156
+ const SHIPPED_ENTRIES: ReadonlySet<string> = new Set([
157
+ 'CLAUDE.md',
158
+ 'statusline.sh',
159
+ 'agents',
160
+ 'output-styles',
161
+ 'skills',
162
+ ]);
163
+
164
+ // A file's bytes, with an executable mode when its owner may execute it.
165
+ function readBundleFile(path: string, mode: number): GuestFile {
166
+ const content = readFileSync(path);
167
+
168
+ return (mode & 0o100) === 0 ? content : { content, mode: 0o755 };
169
+ }
package/src/cli.ts CHANGED
@@ -452,10 +452,12 @@ async function runDaemon(listenArg: string | null, tokenFile: string | null): Pr
452
452
  principals: cfg.principals,
453
453
  sources: sources.sources,
454
454
  gitTransports: cfg.workspaces.gitTransports,
455
+ workspaceRoots: { root: cfg.workspaces.root, targetRoots: cfg.workspaces.targetRoots },
455
456
  ...(listen === null || tokenFile === null
456
457
  ? {}
457
458
  : { listen: { host: listen.host, port: listen.port, tokenFile } }),
458
459
  restoreBootTimeoutMs,
460
+ resumeInterruptedTurns: cfg.resumeInterruptedTurns,
459
461
  ...(Number.isFinite(graceOverride) && graceOverride >= 0
460
462
  ? { tapGraceMs: graceOverride }
461
463
  : {}),
@@ -2,6 +2,7 @@ import { match } from 'ts-pattern';
2
2
  import { createActor } from 'xstate';
3
3
  import { countSessionStates } from '../protocol/count-session-states';
4
4
  import { DaemonError } from '../protocol/daemon-error';
5
+ import type { DaemonFeature } from '../protocol/daemon-features';
5
6
  import type { EventMsg } from '../protocol/protocol';
6
7
  import { sortGroupedSessionViews } from '../protocol/sort-grouped-session-views';
7
8
  import { sortSessionViews } from '../protocol/sort-session-views';
@@ -339,12 +340,17 @@ const picker = new SpawnPicker<MirrorSession>({
339
340
  attach,
340
341
  toMirrorSession,
341
342
  upsertMirror,
343
+ hasDaemonFeature,
342
344
  });
343
345
 
344
346
  function openPicker(resume: boolean) {
345
347
  picker.open(resume);
346
348
  }
347
349
 
350
+ function hasDaemonFeature(feature: DaemonFeature): boolean {
351
+ return daemonFeatures.has(feature);
352
+ }
353
+
348
354
  function sendRequest(m: string, p?: Readonly<Record<string, unknown>>) {
349
355
  return client.sendRequest(m, p);
350
356
  }
@@ -832,6 +838,7 @@ const boot = await bootDaemonClient({
832
838
 
833
839
  let client = boot.client;
834
840
  let daemonStale = boot.stale;
841
+ let daemonFeatures = boot.features;
835
842
  let daemonRestarting = false;
836
843
 
837
844
  lastUsedAgent = boot.lastUsedAgent;
@@ -950,6 +957,7 @@ async function restartDaemon() {
950
957
 
951
958
  client = next.client;
952
959
  daemonStale = next.stale;
960
+ daemonFeatures = next.features;
953
961
  lastUsedAgent = next.lastUsedAgent;
954
962
  client.onEvent = applyDaemonEvent;
955
963