@phnx-labs/agents-cli 1.22.46 → 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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- 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/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- 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 +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -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.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- 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 +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- 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 +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- 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/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- 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 +6 -11
- package/dist/lib/config-keys.js +18 -19
- 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/runner.js +15 -2
- 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 +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- 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 +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- 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 +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- 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 +35 -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/hosts/registry.js +7 -0
- 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 +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- 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/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- 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/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.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.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- 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/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- 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.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- 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-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- 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.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- 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.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- 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 +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- 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 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- 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
|
@@ -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
|
@@ -94,6 +94,8 @@ export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
|
94
94
|
export interface ExecOptions {
|
|
95
95
|
agent: AgentId;
|
|
96
96
|
version?: string;
|
|
97
|
+
/** Version home whose native auth/config is overlaid onto this run's binary. */
|
|
98
|
+
configVersion?: string;
|
|
97
99
|
/** Omit to launch the CLI interactively -- no prompt, no --print, stdio fully inherited. */
|
|
98
100
|
prompt?: string;
|
|
99
101
|
/** Force interactive mode even when a prompt is provided. Wins over `headless`. */
|
|
@@ -398,30 +400,67 @@ export interface TmuxWrapContext {
|
|
|
398
400
|
raw: boolean;
|
|
399
401
|
/** The AGENTS_NO_TMUX=1 escape hatch. */
|
|
400
402
|
noTmuxEnv: boolean;
|
|
401
|
-
/** This device's `tmux.enabled` config —
|
|
403
|
+
/** This device's `tmux.enabled` config — true opts every eligible launch on this box into the wrap. */
|
|
402
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;
|
|
403
420
|
/** Whether a tmux binary is on PATH. */
|
|
404
421
|
tmuxAvailable: boolean;
|
|
405
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
|
+
};
|
|
406
442
|
/**
|
|
407
443
|
* Decide whether to run an interactive agent INSIDE a detached tmux session on
|
|
408
444
|
* the shared socket (then attach the current TTY) instead of a bare spawn.
|
|
409
445
|
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
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}).
|
|
414
456
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
421
|
-
* - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
|
|
422
|
-
* - 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.
|
|
423
462
|
*/
|
|
424
|
-
export declare function
|
|
463
|
+
export declare function resolveTmuxWrap(ctx: TmuxWrapContext): TmuxWrapDecision;
|
|
425
464
|
/**
|
|
426
465
|
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
427
466
|
* env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
|
|
@@ -440,6 +479,23 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
|
|
440
479
|
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
441
480
|
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
442
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;
|
|
443
499
|
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
|
|
444
500
|
redactEnvValues?: boolean;
|
|
445
501
|
envFile?: string;
|
package/dist/lib/exec.js
CHANGED
|
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { ALL_MODES } from './types.js';
|
|
11
|
+
import { ALL_MODES, REMOTE_INTERACTIVE_ENV } from './types.js';
|
|
12
12
|
import { AGENTS, findInPath } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './scheduling/routines.js';
|
|
14
14
|
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
|
|
@@ -28,7 +28,7 @@ import { recordRunName } from './session/run-names.js';
|
|
|
28
28
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
29
29
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
30
30
|
import { isTmuxInstalled } from './tmux/binary.js';
|
|
31
|
-
import { isTmuxEnabled } from './device-config.js';
|
|
31
|
+
import { isTmuxEnabled, selfConfiguredDeviceRole } from './device-config.js';
|
|
32
32
|
import { machineId } from './machine-id.js';
|
|
33
33
|
import { shellQuote } from './ssh-exec.js';
|
|
34
34
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
@@ -293,17 +293,28 @@ export function buildExecEnv(options) {
|
|
|
293
293
|
// Resolve version/versionHome here (this module already imports
|
|
294
294
|
// installations/versions); the adapters must not import it themselves or they
|
|
295
295
|
// close a versions -> shims -> harness -> adapter import cycle.
|
|
296
|
-
const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.version);
|
|
296
|
+
const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
|
|
297
297
|
configAdapter.applyExecConfigEnv(result, {
|
|
298
298
|
agent: options.agent,
|
|
299
299
|
version,
|
|
300
300
|
versionHome,
|
|
301
301
|
interactive: resolveInteractive(options),
|
|
302
|
+
deviceRole: selfConfiguredDeviceRole(),
|
|
302
303
|
resolveClaudeSetupToken,
|
|
303
304
|
});
|
|
304
305
|
}
|
|
305
306
|
else {
|
|
306
307
|
stripForeignConfigDir(result);
|
|
308
|
+
// Grok's adapter owns the same GROK_HOME export in generated shims, but a
|
|
309
|
+
// labeled account deliberately runs one version's binary with another
|
|
310
|
+
// version's config. Apply that overlay here because the shim's binary-slot
|
|
311
|
+
// export cannot express a distinct account slot.
|
|
312
|
+
if (options.agent === 'grok' && options.configVersion) {
|
|
313
|
+
const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion);
|
|
314
|
+
if (!versionHome)
|
|
315
|
+
throw new Error(`Cannot resolve Grok account config home for ${options.configVersion}.`);
|
|
316
|
+
result.GROK_HOME = path.join(versionHome, '.grok');
|
|
317
|
+
}
|
|
307
318
|
}
|
|
308
319
|
// Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
|
|
309
320
|
// knows which box to drain and inject mid-run. Keyed by the session id — the
|
|
@@ -322,6 +333,7 @@ export function buildExecEnv(options) {
|
|
|
322
333
|
// every event the child emits. `options.sessionId` is the CHILD's id, so read the
|
|
323
334
|
// spawner from the live env; guard a same-session resume from naming itself parent.
|
|
324
335
|
// Local-spawn scope here; forwarding it across the `--device` SSH hop is Phase 4.
|
|
336
|
+
delete result.AGENTS_PARENT_SESSION_ID;
|
|
325
337
|
const spawnerSessionId = process.env.AGENTS_SESSION_ID || process.env.AGENT_SESSION_ID;
|
|
326
338
|
if (spawnerSessionId && spawnerSessionId !== options.sessionId) {
|
|
327
339
|
result.AGENTS_PARENT_SESSION_ID = spawnerSessionId;
|
|
@@ -388,6 +400,7 @@ export const AGENT_COMMANDS = {
|
|
|
388
400
|
// independent; legacy --sandbox flags cannot express that combination.
|
|
389
401
|
plan: [],
|
|
390
402
|
edit: [],
|
|
403
|
+
auto: [],
|
|
391
404
|
// skip = codex --yolo: drops the sandbox entirely and approves anything.
|
|
392
405
|
skip: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
393
406
|
},
|
|
@@ -1163,36 +1176,47 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
|
|
|
1163
1176
|
* Decide whether to run an interactive agent INSIDE a detached tmux session on
|
|
1164
1177
|
* the shared socket (then attach the current TTY) instead of a bare spawn.
|
|
1165
1178
|
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1179
|
+
* Wrapping serves two independent purposes, and they are gated differently:
|
|
1180
|
+
*
|
|
1181
|
+
* - **Addressability** (`configEnabled`): a unique `%pane` handle so
|
|
1182
|
+
* `agents sessions --active` can tell co-located agents apart and `agents
|
|
1183
|
+
* focus` re-attaches without forking. That is a local preference — the
|
|
1184
|
+
* operator turns it on once tmux's mouse/clipboard/scrollback suits them.
|
|
1185
|
+
* - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
|
|
1186
|
+
* the SSH client, because a bare remote spawn dies of SIGHUP the moment the
|
|
1187
|
+
* link blinks. That is not a preference, so it does not consult
|
|
1188
|
+
* `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
|
|
1189
|
+
*
|
|
1190
|
+
* The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
|
|
1191
|
+
* explicit "I want the bare process" requests, and honouring them over the
|
|
1192
|
+
* durability rule keeps one escape hatch that always works.
|
|
1170
1193
|
*
|
|
1171
|
-
*
|
|
1172
|
-
* - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
|
|
1173
|
-
* - not Windows — no tmux path on win32.
|
|
1174
|
-
* - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
|
|
1175
|
-
* - not --raw — explicit opt-out.
|
|
1176
|
-
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
1177
|
-
* - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
|
|
1178
|
-
* - tmux installed — otherwise there is nothing to wrap with.
|
|
1194
|
+
* Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
|
|
1179
1195
|
*/
|
|
1180
|
-
export function
|
|
1196
|
+
export function resolveTmuxWrap(ctx) {
|
|
1197
|
+
// A headless `-p` run has no TTY to attach, Windows has no tmux path, and
|
|
1198
|
+
// nesting tmux-in-tmux is pointless — none of these can wrap, and none of them
|
|
1199
|
+
// is a remote interactive agent whose life depends on it.
|
|
1181
1200
|
if (!ctx.interactive)
|
|
1182
|
-
return
|
|
1201
|
+
return { kind: 'bare' };
|
|
1183
1202
|
if (ctx.platform === 'win32')
|
|
1184
|
-
return
|
|
1203
|
+
return { kind: 'bare' };
|
|
1185
1204
|
if (ctx.inTmux)
|
|
1186
|
-
return
|
|
1205
|
+
return { kind: 'bare' };
|
|
1206
|
+
// Explicit opt-outs win over the durability rule: `--raw` exists precisely to
|
|
1207
|
+
// get the unwrapped process, and a flag that silently stopped working on a
|
|
1208
|
+
// remote box would be worse than an undurable run the user asked for.
|
|
1187
1209
|
if (ctx.raw)
|
|
1188
|
-
return
|
|
1210
|
+
return { kind: 'bare' };
|
|
1189
1211
|
if (ctx.noTmuxEnv)
|
|
1190
|
-
return
|
|
1191
|
-
if (!ctx.configEnabled)
|
|
1192
|
-
return
|
|
1212
|
+
return { kind: 'bare' };
|
|
1213
|
+
if (!ctx.configEnabled && !ctx.remoteDispatch)
|
|
1214
|
+
return { kind: 'bare' };
|
|
1215
|
+
// Fail loud rather than launch a remote agent that a blink would kill: the
|
|
1216
|
+
// caller refuses the run instead of starting work that cannot be recovered.
|
|
1193
1217
|
if (!ctx.tmuxAvailable)
|
|
1194
|
-
return
|
|
1195
|
-
return
|
|
1218
|
+
return ctx.remoteDispatch ? { kind: 'undurable' } : { kind: 'bare' };
|
|
1219
|
+
return { kind: 'wrap' };
|
|
1196
1220
|
}
|
|
1197
1221
|
/**
|
|
1198
1222
|
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
@@ -1212,6 +1236,29 @@ export function shouldWrapInTmux(ctx) {
|
|
|
1212
1236
|
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
1213
1237
|
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
1214
1238
|
*/
|
|
1239
|
+
/**
|
|
1240
|
+
* True when `options.sessionId` is an id the HARNESS actually received, and so a
|
|
1241
|
+
* real, resumable handle — rather than one the launcher generated for its own
|
|
1242
|
+
* bookkeeping and the agent never saw.
|
|
1243
|
+
*
|
|
1244
|
+
* Mirrors exactly the two branches in buildExecCommand that put the id on the
|
|
1245
|
+
* command line: a native resume (any harness with a `resume` spec) and claude's
|
|
1246
|
+
* create-with-`--session-id`. Everything else — `agents run codex --session-id X`
|
|
1247
|
+
* without `--resume`, or a fresh non-claude launch whose real id only shows up
|
|
1248
|
+
* later via the SessionStart hook — leaves the agent with an id of its own
|
|
1249
|
+
* choosing, so `options.sessionId` must not be published as if it were that id.
|
|
1250
|
+
*
|
|
1251
|
+
* Keep in lockstep with buildExecCommand's resume/create branches: a harness
|
|
1252
|
+
* that gains a forced-session-id flag must be reflected here in the SAME change,
|
|
1253
|
+
* or this reports a genuine id as fabricated.
|
|
1254
|
+
*/
|
|
1255
|
+
export function isHarnessKnownSessionId(agent, sessionId, resume) {
|
|
1256
|
+
if (!sessionId)
|
|
1257
|
+
return false;
|
|
1258
|
+
if (resume && AGENT_COMMANDS[agent]?.resume)
|
|
1259
|
+
return true;
|
|
1260
|
+
return agent === 'claude';
|
|
1261
|
+
}
|
|
1215
1262
|
export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
|
|
1216
1263
|
const agentCmd = [executable, ...args].map(shellQuote).join(' ');
|
|
1217
1264
|
// envFile: source the values instead of inlining them, so no VALUE ever lands
|
|
@@ -1282,11 +1329,14 @@ export function formatPaneTail(raw, maxLines = 30) {
|
|
|
1282
1329
|
* Lifecycle:
|
|
1283
1330
|
* 1. createSession() launches `sh -c 'exec env … agent'` detached, remain-on-exit
|
|
1284
1331
|
* on (global), and returns the pane id.
|
|
1285
|
-
* 2. A per-session `pane-died` hook
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1332
|
+
* 2. A per-session `pane-died` hook tears the session down the instant the AGENT
|
|
1333
|
+
* pane exits: with a client attached it `detach-client`s so attach returns
|
|
1334
|
+
* instead of parking on a dead pane (then resolveAfterAttach reads the status
|
|
1335
|
+
* and kills); with no client attached it `kill-session`s outright, so a
|
|
1336
|
+
* wrapped agent that exits unattended can't leave a dead husk lingering on the
|
|
1337
|
+
* socket. The hook is guarded on `#{hook_pane}` so it fires ONLY for the agent
|
|
1338
|
+
* pane — user-created splits (Ctrl-b " / %) that the user exits are closed in
|
|
1339
|
+
* place (`kill-pane`) instead of tearing down the whole client, so exiting one
|
|
1290
1340
|
* split leaves the agent running full-window rather than kicking you out.
|
|
1291
1341
|
* 3. We record the agent pane's pid → session mapping (WITH the tmux pane) so the
|
|
1292
1342
|
* headless active-scan attributes it, then attach the TTY (blocking).
|
|
@@ -1303,7 +1353,7 @@ async function runInTmux(options, executable, args) {
|
|
|
1303
1353
|
const idSeed = (options.sessionId ?? randomUUID()).slice(0, 8);
|
|
1304
1354
|
const name = slugifyName(`ag-${options.agent}-${idSeed}`);
|
|
1305
1355
|
const RED = '\x1b[31m', GRAY = '\x1b[90m', OFF = '\x1b[0m';
|
|
1306
|
-
const NO_TMUX_TIP = `${GRAY}
|
|
1356
|
+
const NO_TMUX_TIP = `${GRAY} This run used the opt-in tmux wrap. Re-run with --no-tmux for a direct launch, or turn the wrap off: agents config set devices.${machineId()}.tmux off${OFF}\n\n`;
|
|
1307
1357
|
// Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
|
|
1308
1358
|
// detaches the client the instant the agent exits, so a fast failure (a
|
|
1309
1359
|
// gutted install that dies with ENOENT, a bad flag, a crash on startup) would
|
|
@@ -1414,8 +1464,13 @@ async function runInTmux(options, executable, args) {
|
|
|
1414
1464
|
const cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
|
|
1415
1465
|
const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
|
|
1416
1466
|
const labels = { agent: options.agent };
|
|
1417
|
-
|
|
1467
|
+
// Only publish an id the harness actually received. createSession turns this
|
|
1468
|
+
// label into the `@ag_session_id` tmux option that status bars read, and a
|
|
1469
|
+
// launcher-internal id displayed there looks exactly like a resumable one
|
|
1470
|
+
// while `ag focus <id>` rejects it.
|
|
1471
|
+
if (isHarnessKnownSessionId(options.agent, options.sessionId, options.resume)) {
|
|
1418
1472
|
labels.sessionId = options.sessionId;
|
|
1473
|
+
}
|
|
1419
1474
|
// Only a launched pane sources-and-unlinks the env file. If createSession
|
|
1420
1475
|
// throws, the pane never runs, so the resolved secrets (incl. the master
|
|
1421
1476
|
// passphrase) would linger on disk — remove it on that failure path
|
|
@@ -1600,19 +1655,36 @@ async function spawnAgent(options) {
|
|
|
1600
1655
|
command: executable,
|
|
1601
1656
|
args: redactArgs(args.slice(0, 10)),
|
|
1602
1657
|
});
|
|
1603
|
-
// Interactive spawn-wrap:
|
|
1604
|
-
//
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
|
|
1658
|
+
// Interactive spawn-wrap: run the agent INSIDE a shared-socket tmux session
|
|
1659
|
+
// (then attach this TTY) so it gets a unique, addressable %pane — and, when
|
|
1660
|
+
// the run arrived over `--device`, so it survives the SSH link that carries
|
|
1661
|
+
// it. Every failed guard keeps the bare spawn below.
|
|
1662
|
+
const tmuxWrap = resolveTmuxWrap({
|
|
1608
1663
|
interactive,
|
|
1609
1664
|
platform: process.platform,
|
|
1610
1665
|
inTmux: !!process.env.TMUX,
|
|
1611
1666
|
raw: options.raw === true,
|
|
1612
1667
|
noTmuxEnv: process.env.AGENTS_NO_TMUX === '1',
|
|
1613
1668
|
configEnabled: isTmuxEnabled(),
|
|
1669
|
+
remoteDispatch: process.env[REMOTE_INTERACTIVE_ENV] === '1',
|
|
1614
1670
|
tmuxAvailable: isTmuxInstalled(),
|
|
1615
|
-
})
|
|
1671
|
+
});
|
|
1672
|
+
if (tmuxWrap.kind === 'undurable') {
|
|
1673
|
+
// Refuse rather than start work a blink would destroy. This is the ONLY
|
|
1674
|
+
// check — `ensureHostReady` gates a `--device` dispatch on the peer being
|
|
1675
|
+
// reachable and able to run the pinned agent, not on it having tmux, so
|
|
1676
|
+
// there is no launcher-side probe ahead of this one. Failing here still
|
|
1677
|
+
// fails before the agent starts, which is what matters; a pre-dispatch
|
|
1678
|
+
// probe would only move the message earlier, at the cost of a round trip on
|
|
1679
|
+
// every launch.
|
|
1680
|
+
const msg = `agents: ${machineId()} has no tmux, so a --device run here could not survive a dropped connection.\n`
|
|
1681
|
+
+ ` install it: (apt|dnf|brew) install tmux\n`
|
|
1682
|
+
+ ` or accept the risk: agents run … --device ${machineId()} --raw\n`;
|
|
1683
|
+
process.stderr.write(`\x1b[31m${msg}\x1b[0m`);
|
|
1684
|
+
timer.end({ exitCode: 1, status: 'failed', error: 'remote interactive run has no tmux for durability' });
|
|
1685
|
+
return { exitCode: 1, stdout: '', stderr: msg };
|
|
1686
|
+
}
|
|
1687
|
+
if (tmuxWrap.kind === 'wrap') {
|
|
1616
1688
|
timer.mark('startup');
|
|
1617
1689
|
try {
|
|
1618
1690
|
const result = await runInTmux(options, executable, args);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ActiveSession } from '../session/active.js';
|
|
2
|
+
import { type MessageReceipt } from './feed.js';
|
|
3
|
+
export interface VerifiedOperator {
|
|
4
|
+
id?: string;
|
|
5
|
+
verified: boolean;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FeedAnswerResult {
|
|
9
|
+
status: 'delivered' | 'already_answered';
|
|
10
|
+
receipt: MessageReceipt;
|
|
11
|
+
}
|
|
12
|
+
/** Atomically claim the first answer, then route it over the session's recorded reply rail. */
|
|
13
|
+
export declare function claimAndRouteAttentionAnswer(input: {
|
|
14
|
+
attentionKey: string;
|
|
15
|
+
choiceId?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
operator: VerifiedOperator;
|
|
18
|
+
feedRoot?: string;
|
|
19
|
+
mailboxRoot?: string;
|
|
20
|
+
sessions?: ActiveSession[];
|
|
21
|
+
}): Promise<FeedAnswerResult>;
|
|
22
|
+
/** Forward a fleet attention answer to the device that owns its scope. */
|
|
23
|
+
export declare function forwardFeedAnswer(input: {
|
|
24
|
+
host: string;
|
|
25
|
+
attentionKey: string;
|
|
26
|
+
choiceId?: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
operatorId?: string;
|
|
29
|
+
}): Promise<FeedAnswerResult>;
|