@phnx-labs/agents-cli 1.22.47 → 1.22.49
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/CHANGELOG.md +401 -17
- package/README.md +66 -32
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +206 -32
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/browser.js +501 -103
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +0 -20
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/exec.js +52 -15
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +4 -3
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +3 -2
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-trace.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +17 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/accounting/usage.d.ts +14 -3
- package/dist/lib/accounting/usage.js +64 -18
- package/dist/lib/agent-spec/agents.js +11 -9
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.js +8 -3
- package/dist/lib/browser/profiles.d.ts +35 -101
- package/dist/lib/browser/profiles.js +130 -301
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +8 -6
- package/dist/lib/browser/remote-control.js +8 -6
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +40 -16
- package/dist/lib/browser/runtime-state.js +92 -25
- package/dist/lib/browser/service.d.ts +28 -9
- package/dist/lib/browser/service.js +139 -62
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +2 -8
- package/dist/lib/config-keys.js +2 -13
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +20 -18
- package/dist/lib/device-config.d.ts +0 -2
- package/dist/lib/device-config.js +1 -23
- package/dist/lib/devices/registry.d.ts +1 -1
- package/dist/lib/devices/registry.js +1 -1
- package/dist/lib/devices/stats-cache.d.ts +20 -0
- package/dist/lib/devices/stats-cache.js +49 -5
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +67 -13
- package/dist/lib/exec.js +85 -29
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +11 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +19 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +31 -7
- package/dist/lib/menubar/install-menubar.js +79 -21
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.js +17 -1
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +138 -6
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +33 -5
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.js +11 -2
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +88 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-html.d.ts +5 -1
- package/dist/lib/session/trajectory-html.js +42 -22
- package/dist/lib/session/trajectory-text.js +9 -2
- package/dist/lib/session/trajectory.js +53 -4
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +7 -3
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +18 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-refresh.d.ts +1 -1
- package/dist/lib/usage-refresh.js +11 -8
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +4 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -67,8 +67,9 @@ export declare function runFleetCacheWarmTick(): Promise<void>;
|
|
|
67
67
|
/**
|
|
68
68
|
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
69
69
|
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* Every host is its own provider-facing writer — it refreshes only the
|
|
71
|
+
* accounts it holds credentials for, straight from the provider APIs
|
|
72
|
+
* (RUSH-3193 #15; no cross-host broadcast).
|
|
72
73
|
*/
|
|
73
74
|
export declare function runUsageRefreshTick(): Promise<void>;
|
|
74
75
|
/**
|
|
@@ -79,9 +80,19 @@ export declare function runUsageRefreshTick(): Promise<void>;
|
|
|
79
80
|
* Cadence matches {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS} so one-shot readers and
|
|
80
81
|
* long-lived watchers share one writer. Without this tick the journal has no
|
|
81
82
|
* continuous producer and Factory freezes after the initial cache snapshot.
|
|
83
|
+
*
|
|
84
|
+
* Gated on reader presence (RUSH-3193): when no `sessions watch` / `feed watch`
|
|
85
|
+
* consumer has checked in within {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS} the
|
|
86
|
+
* expensive `ps`+`lsof` gather is skipped entirely on this scheduled tick. A
|
|
87
|
+
* watcher connecting to a cold/idle daemon is NOT served by this tick alone —
|
|
88
|
+
* {@link noteActiveSessionsJournalReader} only writes a presence timestamp, with
|
|
89
|
+
* no path back to this timer — so the daemon separately runs
|
|
90
|
+
* {@link watchActiveSessionsReaderPresence} to detect that idle→recent edge and
|
|
91
|
+
* fire an out-of-band call into this same function immediately (RUSH-2484).
|
|
82
92
|
*/
|
|
83
93
|
export declare function runActiveSessionsWarmTick(opts?: {
|
|
84
94
|
gather?: () => Promise<import('./session/active.js').ActiveSession[]>;
|
|
95
|
+
nowMs?: number;
|
|
85
96
|
}): Promise<{
|
|
86
97
|
sessions: number;
|
|
87
98
|
}>;
|
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -108,21 +108,11 @@ export async function runFleetCacheWarmTick() {
|
|
|
108
108
|
/**
|
|
109
109
|
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
110
110
|
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
111
|
+
* Every host is its own provider-facing writer — it refreshes only the
|
|
112
|
+
* accounts it holds credentials for, straight from the provider APIs
|
|
113
|
+
* (RUSH-3193 #15; no cross-host broadcast).
|
|
113
114
|
*/
|
|
114
115
|
export async function runUsageRefreshTick() {
|
|
115
|
-
const { resolveUsagePrimaryHost } = await import('./device-config.js');
|
|
116
|
-
const { machineId } = await import('./machine-id.js');
|
|
117
|
-
// usage.primary-host, else interactive.host, else null (standalone local refresh)
|
|
118
|
-
const primaryHost = resolveUsagePrimaryHost() ?? undefined;
|
|
119
|
-
const self = machineId();
|
|
120
|
-
const { importUsageFleetFromHost, usageRefreshRole } = await import('./usage-fleet.js');
|
|
121
|
-
if (usageRefreshRole(primaryHost, self) === 'subscriber') {
|
|
122
|
-
const imported = await importUsageFleetFromHost(primaryHost);
|
|
123
|
-
console.log(`usage refresh: imported ${Object.keys(imported.usage).length} account(s) from primary host ${primaryHost}`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
116
|
const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
|
|
127
117
|
const { writeClaudeUsageCache } = await import('./accounting/usage.js');
|
|
128
118
|
const { usageRateLimitedUntil } = await import('./usage-backoff.js');
|
|
@@ -134,9 +124,7 @@ export async function runUsageRefreshTick() {
|
|
|
134
124
|
const { listProfiles } = await import('./profiles.js');
|
|
135
125
|
const { refreshDueByokUsage } = await import('./byok-usage.js');
|
|
136
126
|
const byok = await refreshDueByokUsage(listProfiles());
|
|
137
|
-
|
|
138
|
-
const published = exportUsageFleet();
|
|
139
|
-
console.log(`usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped; BYOK ${byok.refreshed} refreshed, ${byok.skipped} not-due; published ${Object.keys(published.usage).length} account(s)`);
|
|
127
|
+
console.log(`usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped; BYOK ${byok.refreshed} refreshed, ${byok.skipped} not-due`);
|
|
140
128
|
}
|
|
141
129
|
/**
|
|
142
130
|
* Active-sessions warm (RUSH-2062 / RUSH-2484): publish THIS host's live session
|
|
@@ -146,10 +134,24 @@ export async function runUsageRefreshTick() {
|
|
|
146
134
|
* Cadence matches {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS} so one-shot readers and
|
|
147
135
|
* long-lived watchers share one writer. Without this tick the journal has no
|
|
148
136
|
* continuous producer and Factory freezes after the initial cache snapshot.
|
|
137
|
+
*
|
|
138
|
+
* Gated on reader presence (RUSH-3193): when no `sessions watch` / `feed watch`
|
|
139
|
+
* consumer has checked in within {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS} the
|
|
140
|
+
* expensive `ps`+`lsof` gather is skipped entirely on this scheduled tick. A
|
|
141
|
+
* watcher connecting to a cold/idle daemon is NOT served by this tick alone —
|
|
142
|
+
* {@link noteActiveSessionsJournalReader} only writes a presence timestamp, with
|
|
143
|
+
* no path back to this timer — so the daemon separately runs
|
|
144
|
+
* {@link watchActiveSessionsReaderPresence} to detect that idle→recent edge and
|
|
145
|
+
* fire an out-of-band call into this same function immediately (RUSH-2484).
|
|
149
146
|
*/
|
|
150
147
|
export async function runActiveSessionsWarmTick(opts = {}) {
|
|
151
|
-
const { publishLocalActiveSessions } = await import('./session/session-cache.js');
|
|
152
|
-
const
|
|
148
|
+
const { publishLocalActiveSessions, isActiveSessionsJournalReaderRecent } = await import('./session/session-cache.js');
|
|
149
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
150
|
+
if (!isActiveSessionsJournalReaderRecent(nowMs)) {
|
|
151
|
+
console.log('active-sessions warm: idle (no recent reader), skipping gather');
|
|
152
|
+
return { sessions: 0 };
|
|
153
|
+
}
|
|
154
|
+
const r = await publishLocalActiveSessions({ gather: opts.gather, nowMs });
|
|
153
155
|
console.log(`active-sessions warm: ${r.sessions.length} session(s) published`);
|
|
154
156
|
return { sessions: r.sessions.length };
|
|
155
157
|
}
|
|
@@ -124,8 +124,6 @@ export declare function listConfig(opts?: ConfigTarget): ConfigEntry[];
|
|
|
124
124
|
/** List user-scope config keys with their values. Used to show inherited settings
|
|
125
125
|
* in per-device views without implying those keys are device-local. */
|
|
126
126
|
export declare function listUserConfig(): ConfigEntry[];
|
|
127
|
-
/** Resolve the explicit usage host, falling back to the user's interactive host. */
|
|
128
|
-
export declare function resolveUsagePrimaryHost(): string | null;
|
|
129
127
|
/**
|
|
130
128
|
* Set a config key (validated). Device-scope keys target this machine unless
|
|
131
129
|
* `opts.device` names a peer; `opts.fleet` writes the fleet-wide defaults layer
|
|
@@ -61,22 +61,6 @@ export const CONFIG_KEYS = [
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
name: 'usage.primary-host',
|
|
66
|
-
yamlKey: 'usagePrimaryHost',
|
|
67
|
-
scope: 'user',
|
|
68
|
-
type: 'string',
|
|
69
|
-
description: 'Device whose usage snapshots are authoritative for fleet-wide usage reporting.',
|
|
70
|
-
validate: (v) => {
|
|
71
|
-
try {
|
|
72
|
-
assertRegistrableDeviceName(v);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
catch (err) {
|
|
76
|
-
return err?.message ?? String(err);
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
64
|
{
|
|
81
65
|
name: 'auto.pool',
|
|
82
66
|
yamlKey: 'autoPool',
|
|
@@ -181,7 +165,7 @@ export const CONFIG_KEYS = [
|
|
|
181
165
|
description: 'Minutes a browser task may sit with no IPC action (navigate, click, type, screenshot, …) before the daemon\'s ' +
|
|
182
166
|
'abandoned-task reaper closes its tabs and marks it done (RUSH-2622). 0 disables idle reaping — the reaper still ' +
|
|
183
167
|
"closes a task whose owning agent session has exited, whatever this is set to. Read only on THIS box's own " +
|
|
184
|
-
'reaper tick and `agents browser
|
|
168
|
+
'reaper tick and `agents browser prune`, so it never applies to a peer.',
|
|
185
169
|
validate: (v) => v >= 0 ? null : 'browser.task-idle-minutes must be >= 0 (0 disables idle reaping).',
|
|
186
170
|
},
|
|
187
171
|
{
|
|
@@ -486,12 +470,6 @@ export function listConfig(opts) {
|
|
|
486
470
|
export function listUserConfig() {
|
|
487
471
|
return CONFIG_KEYS.filter((spec) => spec.scope === 'user').map((spec) => getConfigValue(spec.name));
|
|
488
472
|
}
|
|
489
|
-
/** Resolve the explicit usage host, falling back to the user's interactive host. */
|
|
490
|
-
export function resolveUsagePrimaryHost() {
|
|
491
|
-
return getConfigValue('usage.primary-host').value
|
|
492
|
-
?? getConfigValue('interactive.host').value
|
|
493
|
-
?? null;
|
|
494
|
-
}
|
|
495
473
|
// ─── Writes ───────────────────────────────────────────────────────────────────
|
|
496
474
|
/** The fleet manifest for a defaults write: `devices` materializes as an
|
|
497
475
|
* explicit empty map (NOT 'all') so `agents apply` targets nothing until the
|
|
@@ -107,7 +107,7 @@ export declare function assertValidDeviceName(name: string): void;
|
|
|
107
107
|
*
|
|
108
108
|
* Deliberately separate from {@link assertValidDeviceName}, and called from
|
|
109
109
|
* exactly two kinds of place: `agents devices add <name>`, and the config keys
|
|
110
|
-
* that point AT a device (`interactive.host
|
|
110
|
+
* that point AT a device (`interactive.host`).
|
|
111
111
|
*
|
|
112
112
|
* Not from `upsertDevice`, `addIgnored` or the discovery writers. Those all
|
|
113
113
|
* receive tailnet node names the user never typed — `devices sync` upserts every
|
|
@@ -88,7 +88,7 @@ export function assertValidDeviceName(name) {
|
|
|
88
88
|
*
|
|
89
89
|
* Deliberately separate from {@link assertValidDeviceName}, and called from
|
|
90
90
|
* exactly two kinds of place: `agents devices add <name>`, and the config keys
|
|
91
|
-
* that point AT a device (`interactive.host
|
|
91
|
+
* that point AT a device (`interactive.host`).
|
|
92
92
|
*
|
|
93
93
|
* Not from `upsertDevice`, `addIgnored` or the discovery writers. Those all
|
|
94
94
|
* receive tailnet node names the user never typed — `devices sync` upserts every
|
|
@@ -14,6 +14,26 @@ export declare const SPECS_STALE_MS: number;
|
|
|
14
14
|
export declare function isFreshDeviceStats(stats: DeviceStats, now?: number): boolean;
|
|
15
15
|
/** True when cached core, RAM-total, and disk-total facts remain current. */
|
|
16
16
|
export declare function isFreshDeviceSpecs(stats: DeviceStats, now?: number): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Carry a device's last successfully-probed hardware facts onto a row whose
|
|
19
|
+
* probe just came back unreachable (RUSH-3096).
|
|
20
|
+
*
|
|
21
|
+
* A failed probe knows nothing about the box — `probeDeviceStats` resolves
|
|
22
|
+
* `{ host, reachable: false, fetchedAt }` — and that bare row then replaced the
|
|
23
|
+
* cached one, so `agents devices list` rendered an offline device with an empty
|
|
24
|
+
* `spec` cell and forgot its cores/RAM/disk until it came back up. Cores, total
|
|
25
|
+
* RAM, and root-disk capacity do not change while a machine is down, so they
|
|
26
|
+
* survive the failure; `loadPercent`, `memPercent`, and the free-byte counts are
|
|
27
|
+
* current-state readings and stay absent rather than render a stale number as
|
|
28
|
+
* live.
|
|
29
|
+
*
|
|
30
|
+
* `specsFetchedAt` keeps the moment the facts were actually observed while
|
|
31
|
+
* `fetchedAt` advances to this attempt, so the row reads "this hardware, seen
|
|
32
|
+
* then; unreachable, as of now" instead of backdating a reading that never
|
|
33
|
+
* happened. A reachable probe is returned untouched — its own reading is the
|
|
34
|
+
* truth, including a fact the box has stopped reporting.
|
|
35
|
+
*/
|
|
36
|
+
export declare function retainHardwareFacts(probed: DeviceStats, prior: DeviceStats | undefined): DeviceStats;
|
|
17
37
|
/** Read the whole cache (best-effort; a missing/corrupt file yields an empty map). */
|
|
18
38
|
export declare function readStatsCache(): Record<string, DeviceStats>;
|
|
19
39
|
/**
|
|
@@ -48,6 +48,44 @@ export function isFreshDeviceSpecs(stats, now = Date.now()) {
|
|
|
48
48
|
const fetchedAt = stats.specsFetchedAt ?? stats.fetchedAt;
|
|
49
49
|
return now - fetchedAt <= SPECS_STALE_MS;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Carry a device's last successfully-probed hardware facts onto a row whose
|
|
53
|
+
* probe just came back unreachable (RUSH-3096).
|
|
54
|
+
*
|
|
55
|
+
* A failed probe knows nothing about the box — `probeDeviceStats` resolves
|
|
56
|
+
* `{ host, reachable: false, fetchedAt }` — and that bare row then replaced the
|
|
57
|
+
* cached one, so `agents devices list` rendered an offline device with an empty
|
|
58
|
+
* `spec` cell and forgot its cores/RAM/disk until it came back up. Cores, total
|
|
59
|
+
* RAM, and root-disk capacity do not change while a machine is down, so they
|
|
60
|
+
* survive the failure; `loadPercent`, `memPercent`, and the free-byte counts are
|
|
61
|
+
* current-state readings and stay absent rather than render a stale number as
|
|
62
|
+
* live.
|
|
63
|
+
*
|
|
64
|
+
* `specsFetchedAt` keeps the moment the facts were actually observed while
|
|
65
|
+
* `fetchedAt` advances to this attempt, so the row reads "this hardware, seen
|
|
66
|
+
* then; unreachable, as of now" instead of backdating a reading that never
|
|
67
|
+
* happened. A reachable probe is returned untouched — its own reading is the
|
|
68
|
+
* truth, including a fact the box has stopped reporting.
|
|
69
|
+
*/
|
|
70
|
+
export function retainHardwareFacts(probed, prior) {
|
|
71
|
+
if (probed.reachable || !prior)
|
|
72
|
+
return probed;
|
|
73
|
+
const ncpu = probed.ncpu ?? prior.ncpu;
|
|
74
|
+
const memTotalBytes = probed.memTotalBytes ?? prior.memTotalBytes;
|
|
75
|
+
const diskTotalBytes = probed.diskTotalBytes ?? prior.diskTotalBytes;
|
|
76
|
+
if (ncpu === probed.ncpu &&
|
|
77
|
+
memTotalBytes === probed.memTotalBytes &&
|
|
78
|
+
diskTotalBytes === probed.diskTotalBytes) {
|
|
79
|
+
return probed;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...probed,
|
|
83
|
+
ncpu,
|
|
84
|
+
memTotalBytes,
|
|
85
|
+
diskTotalBytes,
|
|
86
|
+
specsFetchedAt: prior.specsFetchedAt ?? prior.fetchedAt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
51
89
|
function cacheFilePath() {
|
|
52
90
|
return path.join(getCacheDir(), CACHE_FILE);
|
|
53
91
|
}
|
|
@@ -95,7 +133,10 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
95
133
|
const writeCache = opts.writeCache ?? writeStatsCache;
|
|
96
134
|
const self = opts.selfName;
|
|
97
135
|
const now = opts.now ?? Date.now();
|
|
98
|
-
|
|
136
|
+
// Always read the cache, even under --refresh. It is not *served* then, but it
|
|
137
|
+
// is the only record of each box's hardware, and a failed probe must not erase
|
|
138
|
+
// it — see {@link retainHardwareFacts}.
|
|
139
|
+
const cache = readCache();
|
|
99
140
|
const stats = new Map();
|
|
100
141
|
const toProbe = [];
|
|
101
142
|
let servedFromCache = false;
|
|
@@ -105,7 +146,7 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
105
146
|
toProbe.push(d);
|
|
106
147
|
continue;
|
|
107
148
|
}
|
|
108
|
-
const cached = cache[d.name];
|
|
149
|
+
const cached = opts.forceRefresh ? undefined : cache[d.name];
|
|
109
150
|
const cacheFresh = cached && (opts.specsOnly ? isFreshDeviceSpecs(cached, now) : isFreshDeviceStats(cached, now));
|
|
110
151
|
if (cached && cacheFresh) {
|
|
111
152
|
stats.set(d.name, cached);
|
|
@@ -121,8 +162,11 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
121
162
|
const probed = await probeFleet(toProbe, { selfName: self });
|
|
122
163
|
const fresh = {};
|
|
123
164
|
for (const [name, s] of probed) {
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
// A box that just failed to answer keeps the hardware it was last seen
|
|
166
|
+
// with, so this call and the cache it writes both carry the spec.
|
|
167
|
+
const row = retainHardwareFacts(s, cache[name]);
|
|
168
|
+
stats.set(name, row);
|
|
169
|
+
fresh[name] = row;
|
|
126
170
|
}
|
|
127
171
|
if (Object.keys(fresh).length > 0)
|
|
128
172
|
writeCache(fresh);
|
|
@@ -131,7 +175,7 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
131
175
|
// list (e.g. self not registered as an ssh target) — matches the old callers'
|
|
132
176
|
// explicit local fallback.
|
|
133
177
|
if (self && !stats.has(self)) {
|
|
134
|
-
stats.set(self, await probeLocal(self));
|
|
178
|
+
stats.set(self, retainHardwareFacts(await probeLocal(self), cache[self]));
|
|
135
179
|
}
|
|
136
180
|
let oldest = null;
|
|
137
181
|
for (const s of stats.values()) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type DevicePlacementSignal } from '../teams/scheduler.js';
|
|
2
|
+
/** Why a candidate was dropped, for the fail-loud error message. */
|
|
3
|
+
export interface WorkerExclusion {
|
|
4
|
+
device: string;
|
|
5
|
+
reason: 'unreachable' | 'overloaded' | 'wrong-platform' | 'interactive';
|
|
6
|
+
}
|
|
7
|
+
export interface WorkerPickPlan {
|
|
8
|
+
/** The chosen device name. Never the local box unless it is an auto-pool member. */
|
|
9
|
+
device: string;
|
|
10
|
+
/** True when the pick IS this machine (the caller should run in place). */
|
|
11
|
+
isLocal: boolean;
|
|
12
|
+
candidates: Array<{
|
|
13
|
+
device: string;
|
|
14
|
+
loadPercent?: number;
|
|
15
|
+
headroom?: string;
|
|
16
|
+
}>;
|
|
17
|
+
excluded: WorkerExclusion[];
|
|
18
|
+
}
|
|
19
|
+
export interface WorkerPickOptions {
|
|
20
|
+
/** Restrict to these platforms. Defaults to POSIX (`linux`, `macos`). */
|
|
21
|
+
platforms?: string[];
|
|
22
|
+
/** Override the candidate pool (tests). */
|
|
23
|
+
eligibleHosts?: string[];
|
|
24
|
+
localMachine?: string;
|
|
25
|
+
/** Injected probe (tests). */
|
|
26
|
+
probe?: (pool: string[]) => Promise<Map<string, DevicePlacementSignal>>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Platforms a bun/vitest offload can actually run on.
|
|
30
|
+
*
|
|
31
|
+
* Windows is excluded by policy, not by accident: `AGENTS.md` states Windows is
|
|
32
|
+
* not a required PR or ordinary-release platform and `tests.yml` runs it only as
|
|
33
|
+
* a `continue-on-error` post-merge smoke. Silently shipping the tree to
|
|
34
|
+
* `win-mini` would produce a confusing failure for a platform whose result
|
|
35
|
+
* nobody gates on.
|
|
36
|
+
*/
|
|
37
|
+
export declare const POSIX_PLATFORMS: readonly ["linux", "macos"];
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the least-loaded eligible worker.
|
|
40
|
+
*
|
|
41
|
+
* Throws — never returns a degraded answer. An offload helper that quietly
|
|
42
|
+
* decided "no worker, run here" would recreate exactly the failure the offload
|
|
43
|
+
* exists to prevent: the operator believes the work went to the fleet while
|
|
44
|
+
* their laptop is pinned. Callers surface the thrown message verbatim.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveWorkerDevice(opts?: WorkerPickOptions): Promise<WorkerPickPlan>;
|
|
47
|
+
/** The one no-worker message, naming every candidate and why it was dropped. */
|
|
48
|
+
export declare function formatNoWorkerError(excluded: WorkerExclusion[], platforms: Set<string>): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick a worker device for offloaded machine work — the suite, a build, any job
|
|
3
|
+
* that pins a box for minutes and must never land on the machine someone is
|
|
4
|
+
* sitting at.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately NOT `resolveDeviceAuto`. That resolver answers "which box
|
|
7
|
+
* should run this AGENT", so it gates on harness installation and a launch-ready
|
|
8
|
+
* account (`smart-launch.ts:200-210`). A vitest run needs neither: it needs a
|
|
9
|
+
* reachable POSIX box with headroom. Gating a test offload on a signed-in Claude
|
|
10
|
+
* account would exclude a perfectly good worker for a reason that has nothing to
|
|
11
|
+
* do with running tests.
|
|
12
|
+
*
|
|
13
|
+
* What it DOES share, on purpose, is everything that decides *where agents may
|
|
14
|
+
* go*: the same {@link listOnlineDeviceNames} pool (so `role=worker` /
|
|
15
|
+
* `role=personal` marks move this surface too), the same
|
|
16
|
+
* {@link probePoolSignals} reachability+load probe (so one cached fleet probe
|
|
17
|
+
* serves both), and the same {@link pickBestDevice} least-loaded ranking. Before
|
|
18
|
+
* this module, `scripts/test.sh` hand-rolled its own resolver in inline Python
|
|
19
|
+
* and therefore could not offer `--device auto` at all.
|
|
20
|
+
*/
|
|
21
|
+
import { normalizeHost } from '../machine-id.js';
|
|
22
|
+
import { localMachineId } from '../session/origin-machine.js';
|
|
23
|
+
import { loadDevicesSync } from './registry.js';
|
|
24
|
+
import { isAutoPoolMember } from './pool.js';
|
|
25
|
+
import { formatEmptyAutoPoolError, listOnlineDeviceNames, } from '../smart-launch.js';
|
|
26
|
+
import { pickBestDevice } from '../teams/scheduler.js';
|
|
27
|
+
import { probePoolSignals } from '../teams/placement-probe.js';
|
|
28
|
+
/**
|
|
29
|
+
* Platforms a bun/vitest offload can actually run on.
|
|
30
|
+
*
|
|
31
|
+
* Windows is excluded by policy, not by accident: `AGENTS.md` states Windows is
|
|
32
|
+
* not a required PR or ordinary-release platform and `tests.yml` runs it only as
|
|
33
|
+
* a `continue-on-error` post-merge smoke. Silently shipping the tree to
|
|
34
|
+
* `win-mini` would produce a confusing failure for a platform whose result
|
|
35
|
+
* nobody gates on.
|
|
36
|
+
*/
|
|
37
|
+
export const POSIX_PLATFORMS = ['linux', 'macos'];
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the least-loaded eligible worker.
|
|
40
|
+
*
|
|
41
|
+
* Throws — never returns a degraded answer. An offload helper that quietly
|
|
42
|
+
* decided "no worker, run here" would recreate exactly the failure the offload
|
|
43
|
+
* exists to prevent: the operator believes the work went to the fleet while
|
|
44
|
+
* their laptop is pinned. Callers surface the thrown message verbatim.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveWorkerDevice(opts = {}) {
|
|
47
|
+
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
48
|
+
const pool = [...new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost))];
|
|
49
|
+
// The local box joins the pool only when a role does not exclude it — the same
|
|
50
|
+
// rule resolveDeviceAuto applies. A box marked `personal` is marked precisely
|
|
51
|
+
// so long jobs stay off it, and that mark must hold for the suite too.
|
|
52
|
+
if (!pool.includes(local) && isAutoPoolMember(local))
|
|
53
|
+
pool.push(local);
|
|
54
|
+
if (pool.length === 0)
|
|
55
|
+
throw new Error(formatEmptyAutoPoolError());
|
|
56
|
+
const excluded = [];
|
|
57
|
+
const wanted = new Set((opts.platforms ?? POSIX_PLATFORMS).map((p) => p.toLowerCase()));
|
|
58
|
+
const reg = loadDevicesSync();
|
|
59
|
+
const platformOf = (name) => {
|
|
60
|
+
const d = reg[name] ?? reg[normalizeHost(name)]
|
|
61
|
+
?? Object.values(reg).find((p) => p && normalizeHost(p.name) === normalizeHost(name));
|
|
62
|
+
return d?.platform ? String(d.platform).toLowerCase() : undefined;
|
|
63
|
+
};
|
|
64
|
+
// Platform first: it is a static registry fact, so filtering here keeps the
|
|
65
|
+
// live probe off boxes that could never be picked anyway.
|
|
66
|
+
const onPlatform = pool.filter((device) => {
|
|
67
|
+
const platform = platformOf(device);
|
|
68
|
+
// Unknown platform stays a candidate — a registry-only box with no recorded
|
|
69
|
+
// platform is a gap in the registry, not positive evidence it is Windows.
|
|
70
|
+
if (platform && !wanted.has(platform)) {
|
|
71
|
+
excluded.push({ device, reason: 'wrong-platform' });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
if (onPlatform.length === 0)
|
|
77
|
+
throw new Error(formatNoWorkerError(excluded, wanted));
|
|
78
|
+
const signals = await (opts.probe ?? ((p) => probePoolSignals(p)))(onPlatform);
|
|
79
|
+
const eligible = onPlatform.filter((device) => {
|
|
80
|
+
const signal = signals.get(device);
|
|
81
|
+
if (signal?.reachable !== true) {
|
|
82
|
+
excluded.push({ device, reason: 'unreachable' });
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (signal.headroom === 'loaded') {
|
|
86
|
+
excluded.push({ device, reason: 'overloaded' });
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
if (eligible.length === 0)
|
|
92
|
+
throw new Error(formatNoWorkerError(excluded, wanted));
|
|
93
|
+
const device = pickBestDevice(eligible, [], { signals });
|
|
94
|
+
return {
|
|
95
|
+
device,
|
|
96
|
+
isLocal: normalizeHost(device) === local,
|
|
97
|
+
candidates: onPlatform.map((key) => ({
|
|
98
|
+
device: key,
|
|
99
|
+
loadPercent: signals.get(key)?.loadPercent,
|
|
100
|
+
headroom: signals.get(key)?.headroom,
|
|
101
|
+
})),
|
|
102
|
+
excluded,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The one no-worker message, naming every candidate and why it was dropped. */
|
|
106
|
+
export function formatNoWorkerError(excluded, platforms) {
|
|
107
|
+
const detail = excluded.length
|
|
108
|
+
? excluded.map((e) => `${e.device} (${e.reason})`).join(', ')
|
|
109
|
+
: 'none';
|
|
110
|
+
return (`agents: no worker device is available for offloaded work `
|
|
111
|
+
+ `[platforms: ${[...platforms].sort().join(', ')}] — excluded: ${detail}. `
|
|
112
|
+
+ 'Mark a worker with `agents devices role <name> worker`, or name one explicitly.');
|
|
113
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -402,28 +402,65 @@ export interface TmuxWrapContext {
|
|
|
402
402
|
noTmuxEnv: boolean;
|
|
403
403
|
/** This device's `tmux.enabled` config — true opts every eligible launch on this box into the wrap. */
|
|
404
404
|
configEnabled: boolean;
|
|
405
|
+
/**
|
|
406
|
+
* True when this run was dispatched onto this box over SSH by `--device`
|
|
407
|
+
* (the launcher exports {@link REMOTE_INTERACTIVE_ENV}).
|
|
408
|
+
*
|
|
409
|
+
* A remote interactive agent is a child of the sshd session and holds its
|
|
410
|
+
* controlling TTY, so without the wrap a dropped link SIGHUPs it and the work
|
|
411
|
+
* in flight is gone. Durability is therefore NOT a preference the way
|
|
412
|
+
* `configEnabled` is — `tmux.enabled` is about whether this box's operator
|
|
413
|
+
* likes tmux's mouse/clipboard/scrollback at their own keyboard, which says
|
|
414
|
+
* nothing about whether a run arriving over the network must outlive it.
|
|
415
|
+
* Conflating the two is what left every `--device` agent unsurvivable
|
|
416
|
+
* (RUSH-3125), while lib/hosts/reconnect.ts reconnected on the premise that
|
|
417
|
+
* they were detached.
|
|
418
|
+
*/
|
|
419
|
+
remoteDispatch: boolean;
|
|
405
420
|
/** Whether a tmux binary is on PATH. */
|
|
406
421
|
tmuxAvailable: boolean;
|
|
407
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* What to do with an interactive spawn. Three outcomes, not two: a run that
|
|
425
|
+
* MUST be durable and cannot be is neither "wrap" nor "spawn bare" — it is a
|
|
426
|
+
* launch that should not happen, because a bare remote spawn looks fine right
|
|
427
|
+
* up until the link blinks and the agent dies with it.
|
|
428
|
+
*/
|
|
429
|
+
export type TmuxWrapDecision =
|
|
430
|
+
/** Run the agent in a detached tmux session and attach this TTY. */
|
|
431
|
+
{
|
|
432
|
+
kind: 'wrap';
|
|
433
|
+
}
|
|
434
|
+
/** Spawn directly — nothing about this run needs a pane. */
|
|
435
|
+
| {
|
|
436
|
+
kind: 'bare';
|
|
437
|
+
}
|
|
438
|
+
/** Remote-dispatched and tmux is missing on this box: refuse, don't pretend. */
|
|
439
|
+
| {
|
|
440
|
+
kind: 'undurable';
|
|
441
|
+
};
|
|
408
442
|
/**
|
|
409
443
|
* Decide whether to run an interactive agent INSIDE a detached tmux session on
|
|
410
444
|
* the shared socket (then attach the current TTY) instead of a bare spawn.
|
|
411
445
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
446
|
+
* Wrapping serves two independent purposes, and they are gated differently:
|
|
447
|
+
*
|
|
448
|
+
* - **Addressability** (`configEnabled`): a unique `%pane` handle so
|
|
449
|
+
* `agents sessions --active` can tell co-located agents apart and `agents
|
|
450
|
+
* focus` re-attaches without forking. That is a local preference — the
|
|
451
|
+
* operator turns it on once tmux's mouse/clipboard/scrollback suits them.
|
|
452
|
+
* - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
|
|
453
|
+
* the SSH client, because a bare remote spawn dies of SIGHUP the moment the
|
|
454
|
+
* link blinks. That is not a preference, so it does not consult
|
|
455
|
+
* `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
|
|
416
456
|
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
423
|
-
* - tmux.enabled — this device explicitly opted into addressable tmux panes.
|
|
424
|
-
* - tmux installed — otherwise there is nothing to wrap with.
|
|
457
|
+
* The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
|
|
458
|
+
* explicit "I want the bare process" requests, and honouring them over the
|
|
459
|
+
* durability rule keeps one escape hatch that always works.
|
|
460
|
+
*
|
|
461
|
+
* Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
|
|
425
462
|
*/
|
|
426
|
-
export declare function
|
|
463
|
+
export declare function resolveTmuxWrap(ctx: TmuxWrapContext): TmuxWrapDecision;
|
|
427
464
|
/**
|
|
428
465
|
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
429
466
|
* env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
|
|
@@ -442,6 +479,23 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
|
|
442
479
|
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
443
480
|
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
444
481
|
*/
|
|
482
|
+
/**
|
|
483
|
+
* True when `options.sessionId` is an id the HARNESS actually received, and so a
|
|
484
|
+
* real, resumable handle — rather than one the launcher generated for its own
|
|
485
|
+
* bookkeeping and the agent never saw.
|
|
486
|
+
*
|
|
487
|
+
* Mirrors exactly the two branches in buildExecCommand that put the id on the
|
|
488
|
+
* command line: a native resume (any harness with a `resume` spec) and claude's
|
|
489
|
+
* create-with-`--session-id`. Everything else — `agents run codex --session-id X`
|
|
490
|
+
* without `--resume`, or a fresh non-claude launch whose real id only shows up
|
|
491
|
+
* later via the SessionStart hook — leaves the agent with an id of its own
|
|
492
|
+
* choosing, so `options.sessionId` must not be published as if it were that id.
|
|
493
|
+
*
|
|
494
|
+
* Keep in lockstep with buildExecCommand's resume/create branches: a harness
|
|
495
|
+
* that gains a forced-session-id flag must be reflected here in the SAME change,
|
|
496
|
+
* or this reports a genuine id as fabricated.
|
|
497
|
+
*/
|
|
498
|
+
export declare function isHarnessKnownSessionId(agent: AgentId, sessionId: string | undefined, resume: boolean | undefined): boolean;
|
|
445
499
|
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
|
|
446
500
|
redactEnvValues?: boolean;
|
|
447
501
|
envFile?: string;
|