@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
|
@@ -4,12 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A remote interactive agent runs in a DETACHED tmux session on the peer (see
|
|
6
6
|
* lib/exec.ts `runInTmux`), so a network blink kills only the local ssh client —
|
|
7
|
-
* the agent keeps running.
|
|
7
|
+
* the agent keeps running.
|
|
8
|
+
*
|
|
9
|
+
* **That premise is a guarantee, not a hope, only since RUSH-3125.** It used to
|
|
10
|
+
* rest on the peer's `tmux.enabled`, an ergonomics preference that defaults OFF
|
|
11
|
+
* — so on a default box the agent was a child of the sshd session, a blink
|
|
12
|
+
* SIGHUPed it, and this file reconnected to a corpse while telling the user it
|
|
13
|
+
* was "still running there." The interactive dispatch now exports
|
|
14
|
+
* REMOTE_INTERACTIVE_ENV and exec.ts `resolveTmuxWrap` wraps on it regardless of
|
|
15
|
+
* that toggle, so what is written below actually holds. Anything that would let
|
|
16
|
+
* a remote interactive run reach the peer unwrapped breaks this whole file.
|
|
17
|
+
*
|
|
18
|
+
* `sshStream` reports that drop as exit code 255 (ssh's
|
|
8
19
|
* own connection-layer failure; see ssh-exec.ts). Without this, exec.ts would
|
|
9
20
|
* `process.exit(255)` and the user would have to notice, find the session id, and
|
|
10
21
|
* `agents sessions focus` by hand. Instead we re-attach the live remote pane over
|
|
11
22
|
* SSH automatically, with bounded backoff, until the user detaches cleanly (the
|
|
12
|
-
* remote returns 0)
|
|
23
|
+
* remote returns 0), the agent exits (the tmux session is gone; a non-255 code),
|
|
24
|
+
* or the user interrupts the wait with Ctrl-C ({@link waitOrInterrupt} → 130).
|
|
13
25
|
*
|
|
14
26
|
* The re-attach reuses the peer's OWN recovery verb — `agents sessions focus <id>
|
|
15
27
|
* --local` — which JOINS the live local tmux pane there (a second client, no fork)
|
|
@@ -32,7 +44,8 @@
|
|
|
32
44
|
* {@link MIN_HOLD_MS} refills the retry budget, so a long session that blinks all
|
|
33
45
|
* day keeps reconnecting. Everything else — never reached the host, or reached it
|
|
34
46
|
* and died right back — counts against the budget, so a sustained outage and a
|
|
35
|
-
* fast-flapping link both give up
|
|
47
|
+
* fast-flapping link both give up once {@link RECONNECT_WINDOW_MS} of unproductive
|
|
48
|
+
* retrying has elapsed.
|
|
36
49
|
*
|
|
37
50
|
* The retry policy is a pure state machine (`reconnectStep`) so it is unit-tested
|
|
38
51
|
* without touching SSH; the loop (`reconnectInteractiveSession`) only adds the real
|
|
@@ -47,7 +60,7 @@
|
|
|
47
60
|
* that same 255, `reconnectStep` couldn't tell it apart from a genuine drop, and
|
|
48
61
|
* `connected: true` would refill the retry budget forever — "attempt 1/N" printed
|
|
49
62
|
* on every single cycle, the terminal filling with aborted-TTY escape-code
|
|
50
|
-
* garbage,
|
|
63
|
+
* garbage, the retry window never actually bounding anything.
|
|
51
64
|
*
|
|
52
65
|
* The resume fall-through only widens that surface — the peer's focus now runs a
|
|
53
66
|
* full recovery path (`resumeSessionInPlace` / `runOnPeer`) on a dead pane, any
|
|
@@ -63,8 +76,8 @@
|
|
|
63
76
|
* remap alone did not close it (agents-cli#1884): `connected` was set by the
|
|
64
77
|
* preflight probe and said nothing about whether the attach that followed held, so
|
|
65
78
|
* a fast-flapping link — or an attach that died at the TTY-negotiation stage every
|
|
66
|
-
* single time — refilled the budget on every cycle, printed "attempt 1
|
|
67
|
-
* and left
|
|
79
|
+
* single time — refilled the budget on every cycle, printed "attempt 1" forever,
|
|
80
|
+
* and left the retry window bounding nothing. The {@link MIN_HOLD_MS} floor above is
|
|
68
81
|
* what closes it: an attach that dies immediately is not a reconnection, so the
|
|
69
82
|
* budget drains and the loop gives up with {@link unstableNotice}. A flat total-
|
|
70
83
|
* attempt or wall-clock ceiling that ignored `connected` was the alternative and is
|
|
@@ -72,21 +85,37 @@
|
|
|
72
85
|
* session this feature exists for, while the hold floor only ever stops a loop that
|
|
73
86
|
* is failing to put the user back into the agent.
|
|
74
87
|
*/
|
|
75
|
-
import { sshExec, sshStream, shellQuote } from '../ssh-exec.js';
|
|
88
|
+
import { sshExec, sshStream, shellQuote, SSH_CONN_FAILURE_CODE } from '../ssh-exec.js';
|
|
76
89
|
import { hostIdentityArgs, sshTargetFor } from './types.js';
|
|
90
|
+
import { RUN_AUTO_KEYWORD } from '../types.js';
|
|
77
91
|
/** ssh's connection-layer failure code — the signal that the link dropped rather
|
|
78
|
-
* than the remote command exiting on its own.
|
|
79
|
-
|
|
92
|
+
* than the remote command exiting on its own. Re-exported from ssh-exec.ts, which
|
|
93
|
+
* owns the ssh invocation, so the two cannot drift apart. */
|
|
94
|
+
export const SSH_CONN_FAILURE = SSH_CONN_FAILURE_CODE;
|
|
80
95
|
/** What a would-be-255 remote-origin exit code is remapped to by
|
|
81
96
|
* {@link wrapRemoteExitCode} — see the file header. Never produced by the ssh
|
|
82
97
|
* transport itself, so it can never be confused with {@link SSH_CONN_FAILURE}. */
|
|
83
98
|
export const REMOTE_EXIT_255_REMAPPED = 254;
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
99
|
+
/**
|
|
100
|
+
* How long the loop keeps trying across an UNPRODUCTIVE streak before giving up.
|
|
101
|
+
*
|
|
102
|
+
* A wall-clock window, not an attempt count, because what it has to outlast is
|
|
103
|
+
* measured in minutes: a laptop lid close, a Wi-Fi handoff, a VPN or Tailscale
|
|
104
|
+
* re-auth, a router reboot. The previous `MAX_ATTEMPTS = 6` over a 2/4/8/16/30/30
|
|
105
|
+
* backoff gave up after about **90 seconds** — shorter than any of them
|
|
106
|
+
* (RUSH-3125). Worse, timers are suspended across sleep, so on wake the whole
|
|
107
|
+
* backoff fired back-to-back before the network was up and the budget was gone in
|
|
108
|
+
* seconds.
|
|
109
|
+
*
|
|
110
|
+
* It bounds the STREAK, not the session: a reattach that reconnects and HOLDS
|
|
111
|
+
* resets it to zero ({@link refillsBudget}), so a session that blinks all day
|
|
112
|
+
* still reconnects every time. That is the property the file header insists on,
|
|
113
|
+
* and a flat total would break it — this changes only how a *streak* is bounded.
|
|
114
|
+
*/
|
|
115
|
+
export const RECONNECT_WINDOW_MS = 15 * 60_000;
|
|
116
|
+
/** Backoff curve for a streak: 2s, 4s, 8s, 16s, 30s, then 30s until the window
|
|
117
|
+
* closes. Capped so a long outage keeps probing at a useful cadence instead of
|
|
118
|
+
* drifting out to multi-minute gaps and missing the moment the link returns. */
|
|
90
119
|
const BASE_BACKOFF_MS = 2_000;
|
|
91
120
|
const MAX_BACKOFF_MS = 30_000;
|
|
92
121
|
/** How long a reattach must hold the remote pane before it counts as a genuine
|
|
@@ -98,7 +127,7 @@ const MAX_BACKOFF_MS = 30_000;
|
|
|
98
127
|
* should stop retrying, not one it should keep re-entering. */
|
|
99
128
|
export const MIN_HOLD_MS = 10_000;
|
|
100
129
|
export function initialReconnectState() {
|
|
101
|
-
return { attempt: 0 };
|
|
130
|
+
return { attempt: 0, unproductiveMs: 0 };
|
|
102
131
|
}
|
|
103
132
|
/** Exponential backoff capped at {@link MAX_BACKOFF_MS}: 2s, 4s, 8s, 16s, 30s… */
|
|
104
133
|
export function backoffMs(attempt) {
|
|
@@ -124,27 +153,55 @@ export function refillsBudget(outcome) {
|
|
|
124
153
|
* - a 255 from an attempt that reconnected AND held ({@link refillsBudget})
|
|
125
154
|
* refills the budget first; every other 255 counts against it, so a host that
|
|
126
155
|
* stays unreachable — and a link that keeps dropping the attach immediately —
|
|
127
|
-
* both give up
|
|
156
|
+
* both give up once the retry window closes.
|
|
128
157
|
*/
|
|
129
158
|
export function reconnectStep(state, outcome) {
|
|
130
159
|
if (outcome.code !== SSH_CONN_FAILURE)
|
|
131
160
|
return { action: 'stop', code: outcome.code };
|
|
132
|
-
const
|
|
133
|
-
|
|
161
|
+
const productive = refillsBudget(outcome);
|
|
162
|
+
const attempts = productive ? 0 : state.attempt;
|
|
163
|
+
// The attach's own duration counts against the window: a 10s connect timeout
|
|
164
|
+
// burned on every attempt is real elapsed time the user is waiting, and
|
|
165
|
+
// ignoring it would stretch a "15 minute" window well past fifteen minutes.
|
|
166
|
+
const burned = productive ? 0 : state.unproductiveMs + outcome.heldMs;
|
|
167
|
+
if (burned >= RECONNECT_WINDOW_MS)
|
|
134
168
|
return { action: 'stop', code: SSH_CONN_FAILURE };
|
|
135
|
-
|
|
169
|
+
const waitMs = backoffMs(attempts);
|
|
170
|
+
return {
|
|
171
|
+
action: 'retry',
|
|
172
|
+
waitMs,
|
|
173
|
+
state: { attempt: attempts + 1, unproductiveMs: burned + waitMs },
|
|
174
|
+
remainingMs: Math.max(0, RECONNECT_WINDOW_MS - (burned + waitMs)),
|
|
175
|
+
};
|
|
136
176
|
}
|
|
137
|
-
/**
|
|
138
|
-
export function
|
|
177
|
+
/** "14m51s", "51s" — a duration a waiting human can read at a glance. */
|
|
178
|
+
export function formatDuration(ms) {
|
|
179
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
180
|
+
const m = Math.floor(total / 60);
|
|
181
|
+
const sec = total % 60;
|
|
182
|
+
return m > 0 ? `${m}m${String(sec).padStart(2, '0')}s` : `${sec}s`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Notice shown before each reconnect wait.
|
|
186
|
+
*
|
|
187
|
+
* Says how long is left in the window rather than "attempt 2/6": with a
|
|
188
|
+
* wall-clock budget the attempt number no longer tells the user when this stops,
|
|
189
|
+
* and "how much longer will it keep trying" is the actual question during an
|
|
190
|
+
* outage. Ctrl-C is advertised because a user who wants their shell back should
|
|
191
|
+
* not have to guess whether interrupting is safe — it is (the agent keeps
|
|
192
|
+
* running on the peer, which is the whole point).
|
|
193
|
+
*/
|
|
194
|
+
export function reconnectNotice(target, host, attempt, waitMs, remainingMs) {
|
|
139
195
|
const secs = Math.round(waitMs / 1000);
|
|
140
|
-
const when = secs <= 1 ? 'now' : `in ${secs}
|
|
141
|
-
return `\nConnection to ${host} dropped —
|
|
196
|
+
const when = secs <= 1 ? 'now' : `in ${secs}s`;
|
|
197
|
+
return `\nConnection to ${host} dropped — ${targetLabel(target)} is still running there.`
|
|
198
|
+
+ `\n Reconnecting ${when} · ${formatDuration(remainingMs)} left · attempt ${attempt} · Ctrl-C to stop\n`;
|
|
142
199
|
}
|
|
143
200
|
/** Notice shown once the retry budget is spent on a host that stayed UNREACHABLE.
|
|
144
201
|
* Hands back the one verb that re-enters the terminal — attach the live pane if it
|
|
145
202
|
* survived, else resume. */
|
|
146
|
-
export function exhaustedNotice(
|
|
147
|
-
return `\nCouldn't reconnect to ${host} after ${
|
|
203
|
+
export function exhaustedNotice(target, host) {
|
|
204
|
+
return `\nCouldn't reconnect to ${host} after ${formatDuration(RECONNECT_WINDOW_MS)}. The agent may still be running — get back in when the network is back:\n${recoveryHint(target, host)}`;
|
|
148
205
|
}
|
|
149
206
|
/** Notice shown when the budget is spent the OTHER way: the last reattach reached
|
|
150
207
|
* the host and the connection dropped again within {@link MIN_HOLD_MS}. Saying
|
|
@@ -152,17 +209,23 @@ export function exhaustedNotice(sessionId, host) {
|
|
|
152
209
|
* — and the user needs to know the link, not the host, is the problem. It claims
|
|
153
210
|
* no count of successful reconnections: the budget can also be spent by a run of
|
|
154
211
|
* unreachable attempts followed by one that reconnected and dropped straight out. */
|
|
155
|
-
export function unstableNotice(
|
|
212
|
+
export function unstableNotice(target, host) {
|
|
156
213
|
const secs = Math.round(MIN_HOLD_MS / 1000);
|
|
157
|
-
return `\nGave up reconnecting to ${host} after ${
|
|
214
|
+
return `\nGave up reconnecting to ${host} after ${formatDuration(RECONNECT_WINDOW_MS)} — it kept dropping again within ${secs} seconds of getting back in. The agent may still be running there; reconnect once the link is stable:\n${recoveryHint(target, host)}`;
|
|
158
215
|
}
|
|
159
216
|
/** Notice shown when a reattach stops on a remapped remote-side exit
|
|
160
217
|
* ({@link REMOTE_EXIT_255_REMAPPED} — a would-be-255 the remote command decided
|
|
161
218
|
* on for its own reasons, not the ssh transport dropping; see
|
|
162
219
|
* {@link wrapRemoteExitCode}). Distinct from {@link exhaustedNotice}, which is
|
|
163
220
|
* only for a genuinely spent retry budget. */
|
|
164
|
-
export function remoteExitNotice(
|
|
165
|
-
return `\nReattach to ${
|
|
221
|
+
export function remoteExitNotice(target, host) {
|
|
222
|
+
return `\nReattach to ${targetLabel(target)} on ${host} ended (not a network drop) — get back in, or check whether it's still live:\n${recoveryHint(target, host)}`;
|
|
223
|
+
}
|
|
224
|
+
/** Shown when the user Ctrl-Cs out of the wait. The agent is untouched — it is
|
|
225
|
+
* detached on the peer — so this says how to get back rather than implying the
|
|
226
|
+
* work was lost. */
|
|
227
|
+
export function interruptedNotice(target, host) {
|
|
228
|
+
return `\nStopped reconnecting. ${targetLabel(target)} is still running on ${host}:\n${recoveryHint(target, host)}`;
|
|
166
229
|
}
|
|
167
230
|
/**
|
|
168
231
|
* Wrap `cmd` in `bash -lc` (the login-shell pattern `buildRemoteAgentsInvocation`
|
|
@@ -185,18 +248,138 @@ export function wrapRemoteExitCode(cmd) {
|
|
|
185
248
|
const guarded = `${cmd}; rc=$?; [ "$rc" = "${SSH_CONN_FAILURE}" ] && rc=${REMOTE_EXIT_255_REMAPPED}; exit "$rc"`;
|
|
186
249
|
return `bash -lc ${shellQuote(guarded)}`;
|
|
187
250
|
}
|
|
251
|
+
/** The short form shown to a human. Both kinds are uuid-shaped, so 8 chars reads the same. */
|
|
252
|
+
export function targetLabel(target) {
|
|
253
|
+
return target.id.slice(0, 8);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The command to hand a user whose reconnect gave up.
|
|
257
|
+
*
|
|
258
|
+
* A `session` target has a real id, so `agents sessions resume <id>` does the
|
|
259
|
+
* same attach-else-recover the loop was attempting. The full id is labeled on
|
|
260
|
+
* its own `Session` line, not only inside the command (RUSH-3227). (It used to
|
|
261
|
+
* say `agents reconnect`, which is deprecated and hidden — `commands/reconnect.ts`
|
|
262
|
+
* — so the advice printed at the worst possible moment was itself stale; RUSH-3125.)
|
|
263
|
+
*
|
|
264
|
+
* A `launch` target has no id a user-facing verb accepts: the mapping lives in
|
|
265
|
+
* the peer's hook records, which is exactly why reconnect uses it. So point at
|
|
266
|
+
* the peer's own resolver rather than inventing a launch-id selector on every
|
|
267
|
+
* local command for a string no human ever types.
|
|
268
|
+
*/
|
|
269
|
+
export function recoveryHint(target, host) {
|
|
270
|
+
// The full id is labeled on its own line: burying it only inside a command is
|
|
271
|
+
// how a dropped SSH tab used to land on a bare shell with nothing copyable
|
|
272
|
+
// (RUSH-3227). Resume still sits underneath so the user can paste one verb.
|
|
273
|
+
if (target.kind === 'session') {
|
|
274
|
+
return ` Session ${target.id}\n Resume: agents sessions resume ${target.id}\n`;
|
|
275
|
+
}
|
|
276
|
+
return ` Launch ${target.id}\n agents ssh ${host} 'agents sessions focus --launch-id ${target.id} --local'\n`
|
|
277
|
+
+ ` or pick it: agents sessions --active\n`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Notice shown when an interactive remote connection has ended and the user is
|
|
281
|
+
* back at a local shell — clean detach, agent exit, or a drop that is NOT
|
|
282
|
+
* about to auto-reconnect. The OpenSSH close line (`Shared connection to …
|
|
283
|
+
* closed.`) names the host and nothing else; this is the handle they need to
|
|
284
|
+
* get back in (RUSH-3227).
|
|
285
|
+
*/
|
|
286
|
+
export function connectionEndedNotice(target, host, opts = {}) {
|
|
287
|
+
const verb = opts.dropped ? 'dropped' : 'closed';
|
|
288
|
+
return `\nConnection to ${host} ${verb}.\n${recoveryHint(target, host)}`;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Banner printed when an interactive `--device` run whose session id is already
|
|
292
|
+
* known (Claude's forced id, or a resume) is about to take the TTY. The TUI
|
|
293
|
+
* will cover this; it survives in scrollback so the id exists *while* the
|
|
294
|
+
* connection exists, not only after it dies (RUSH-3227 plan B). Launch-id
|
|
295
|
+
* targets are not a resume handle — do not print them here.
|
|
296
|
+
*/
|
|
297
|
+
export function connectionStartedNotice(target, host) {
|
|
298
|
+
if (target.kind !== 'session')
|
|
299
|
+
return undefined;
|
|
300
|
+
return `Session ${target.id} on ${host}\n Resume later: agents sessions resume ${target.id}\n`;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The id to print as an interactive `--device` stream starts. Resume is a
|
|
304
|
+
* real session id (`resolveHostSessionId` returns undefined on `--resume`);
|
|
305
|
+
* Claude's forced id is the other. `run auto` is excluded: its forwarded
|
|
306
|
+
* `--session-id` is only real if the remote pick is Claude.
|
|
307
|
+
*/
|
|
308
|
+
export function startConnectionTarget(opts) {
|
|
309
|
+
if (opts.agent === RUN_AUTO_KEYWORD)
|
|
310
|
+
return undefined;
|
|
311
|
+
const id = opts.hostSessionId ?? opts.resumeId;
|
|
312
|
+
return id ? { kind: 'session', id } : undefined;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Decide what happens after an interactive `--device` stream returns.
|
|
316
|
+
*
|
|
317
|
+
* Auto-reconnect only when the link dropped (255) AND the run is tmux-hosted
|
|
318
|
+
* (`willReconnect`). `--raw` is not wrapped, so it never reconnects — but it
|
|
319
|
+
* still prints the session id: the user is at a shell, and EXEC-55 does not
|
|
320
|
+
* exempt raw (RUSH-3227). Bundling the notice behind `!isRaw` was the miss.
|
|
321
|
+
*/
|
|
322
|
+
export function afterInteractiveRemoteExit(opts) {
|
|
323
|
+
if (!opts.target)
|
|
324
|
+
return { reconnect: false, notice: undefined };
|
|
325
|
+
if (opts.willReconnect)
|
|
326
|
+
return { reconnect: true, notice: undefined };
|
|
327
|
+
return {
|
|
328
|
+
reconnect: false,
|
|
329
|
+
notice: connectionEndedNotice(opts.target, opts.host, {
|
|
330
|
+
dropped: opts.exitCode === SSH_CONN_FAILURE,
|
|
331
|
+
}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Choose how to name the dropped run when going back for it.
|
|
336
|
+
*
|
|
337
|
+
* Order matters, and the last clause is the fix (RUSH-3125):
|
|
338
|
+
*
|
|
339
|
+
* 1. `run auto` prefers `resolvedId` — the harness the remote ACTUALLY picked,
|
|
340
|
+
* which the launcher's own `--session-id` (adopted only by Claude) may not
|
|
341
|
+
* name. Every other agent prefers the id the launcher forced.
|
|
342
|
+
* 2. `resumeId` covers a run that was continuing a known session.
|
|
343
|
+
* 3. **`launchId` last, and it is what makes this work at all off-Claude.**
|
|
344
|
+
* Every id above either came from the launcher or was read back over SSH —
|
|
345
|
+
* and that read happens after the stream returned, i.e. over the link that
|
|
346
|
+
* just died, so on a real drop it yields nothing. Falling through to the
|
|
347
|
+
* launch id means the reconnect no longer depends on reaching the host to
|
|
348
|
+
* learn what to reconnect to; the peer resolves it locally instead.
|
|
349
|
+
*
|
|
350
|
+
* Returns undefined only when the launcher has no handle at all (a hookless
|
|
351
|
+
* harness with no forced id), which is the one case reconnect genuinely cannot
|
|
352
|
+
* serve. Pure, so the precedence is unit-tested without SSH.
|
|
353
|
+
*/
|
|
354
|
+
export function pickReconnectTarget(inputs) {
|
|
355
|
+
const { agent, sessionId, resolvedId, resumeId, launchId } = inputs;
|
|
356
|
+
const preferred = agent === RUN_AUTO_KEYWORD
|
|
357
|
+
? resolvedId ?? sessionId
|
|
358
|
+
: sessionId ?? resolvedId;
|
|
359
|
+
const id = preferred ?? resumeId;
|
|
360
|
+
if (id)
|
|
361
|
+
return { kind: 'session', id };
|
|
362
|
+
return launchId ? { kind: 'launch', id: launchId } : undefined;
|
|
363
|
+
}
|
|
188
364
|
/**
|
|
189
365
|
* The remote command a reattach runs — the peer's own recovery verb
|
|
190
|
-
* (`agents sessions focus
|
|
366
|
+
* (`agents sessions focus … --local`), wrapped by {@link wrapRemoteExitCode}
|
|
191
367
|
* so a stray remote-origin 255 (from this command, whatever produces it — see the
|
|
192
368
|
* file header) can never masquerade as a network drop. No `--attach-only`: focus
|
|
193
369
|
* joins the live pane when it survived, else RESUMES the session in place, so a
|
|
194
370
|
* reattach landing after the pane died recovers the agent instead of dead-ending
|
|
195
371
|
* (RUSH-2085). Split out from {@link reattachRemoteSession} so it is unit-tested
|
|
196
372
|
* without SSH — mirrors `remoteAgentsJsonCommand` in lib/remote-agents-json.ts.
|
|
373
|
+
*
|
|
374
|
+
* A `launch` target passes `--launch-id`, which focus resolves against the hook
|
|
375
|
+
* records on the peer itself — no network read from this side, which is the
|
|
376
|
+
* whole point (see {@link ReconnectTarget}).
|
|
197
377
|
*/
|
|
198
|
-
export function reattachRemoteCommand(
|
|
199
|
-
const
|
|
378
|
+
export function reattachRemoteCommand(target) {
|
|
379
|
+
const selector = target.kind === 'launch'
|
|
380
|
+
? ['--launch-id', target.id]
|
|
381
|
+
: [target.id];
|
|
382
|
+
const inner = ['agents', 'sessions', 'focus', ...selector, '--local', '--reconnect-reattach']
|
|
200
383
|
.map(shellQuote)
|
|
201
384
|
.join(' ');
|
|
202
385
|
return wrapRemoteExitCode(inner);
|
|
@@ -212,22 +395,49 @@ export function reattachRemoteCommand(sessionId) {
|
|
|
212
395
|
* clean detach; other = session ended), whether this attempt connected, and how
|
|
213
396
|
* long the attach held — the two inputs {@link refillsBudget} decides on.
|
|
214
397
|
*/
|
|
215
|
-
export function reattachRemoteSession(host,
|
|
216
|
-
const
|
|
398
|
+
export function reattachRemoteSession(host, target) {
|
|
399
|
+
const sshTarget = sshTargetFor(host);
|
|
217
400
|
const extraSshArgs = hostIdentityArgs(host);
|
|
218
401
|
// Fresh (non-multiplexed) reachability probe: code 0 means the handshake actually
|
|
219
402
|
// completed, so a hung/failed connect is never mistaken for a live reconnection.
|
|
220
403
|
// RUSH-2265: pass host identity on every hop (probe + stream), not only the first.
|
|
221
|
-
const probe = sshExec(
|
|
404
|
+
const probe = sshExec(sshTarget, 'true', { multiplex: false, extraSshArgs });
|
|
222
405
|
if (probe.code !== 0)
|
|
223
406
|
return { code: SSH_CONN_FAILURE, connected: false, heldMs: 0 };
|
|
224
407
|
// Timed from AFTER the probe returned, so this is the attach's own duration and
|
|
225
408
|
// carries none of the connect phase the file header rules out as a signal.
|
|
226
409
|
const startedAt = Date.now();
|
|
227
|
-
const code = sshStream(
|
|
410
|
+
const code = sshStream(sshTarget, reattachRemoteCommand(target), { tty: true, extraSshArgs });
|
|
228
411
|
return { code, connected: true, heldMs: Date.now() - startedAt };
|
|
229
412
|
}
|
|
230
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Wait `ms`, but return early if the user interrupts.
|
|
415
|
+
*
|
|
416
|
+
* Without this, Ctrl-C during the backoff killed the whole `agents` process:
|
|
417
|
+
* node's default SIGINT handler exits, so the loop never got to say what
|
|
418
|
+
* happened, the terminal was left mid-notice, and the user was dropped at a bare
|
|
419
|
+
* shell with no hint that the agent was still alive on the peer — the same
|
|
420
|
+
* dead-end the reconnect exists to prevent. The handler is installed only for
|
|
421
|
+
* the duration of the wait and always removed, so it can never swallow a Ctrl-C
|
|
422
|
+
* meant for the attached agent (during an attach, ssh owns the tty and this
|
|
423
|
+
* process is not in the foreground group anyway).
|
|
424
|
+
*/
|
|
425
|
+
async function waitOrInterrupt(ms) {
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
let done = false;
|
|
428
|
+
const finish = (how) => {
|
|
429
|
+
if (done)
|
|
430
|
+
return;
|
|
431
|
+
done = true;
|
|
432
|
+
clearTimeout(timer);
|
|
433
|
+
process.removeListener('SIGINT', onSigint);
|
|
434
|
+
resolve(how);
|
|
435
|
+
};
|
|
436
|
+
const onSigint = () => finish('interrupted');
|
|
437
|
+
const timer = setTimeout(() => finish('elapsed'), ms);
|
|
438
|
+
process.on('SIGINT', onSigint);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
231
441
|
/**
|
|
232
442
|
* Drive the reconnect loop from the initial run's outcome to a terminal code.
|
|
233
443
|
* Only the real SSH re-attach and the wait are side effects; the decision is
|
|
@@ -235,7 +445,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
235
445
|
*/
|
|
236
446
|
export async function reconnectInteractiveSession(opts) {
|
|
237
447
|
const write = opts.write ?? ((s) => process.stderr.write(s));
|
|
238
|
-
const wait = opts.wait ??
|
|
448
|
+
const wait = opts.wait ?? waitOrInterrupt;
|
|
239
449
|
const reattach = opts.reattach ?? reattachRemoteSession;
|
|
240
450
|
let state = initialReconnectState();
|
|
241
451
|
// The initial run reached the point of running the agent, so it connected. Its
|
|
@@ -251,17 +461,28 @@ export async function reconnectInteractiveSession(opts) {
|
|
|
251
461
|
// and a link that reconnected but kept dropping reads as exactly that.
|
|
252
462
|
if (decision.code === SSH_CONN_FAILURE) {
|
|
253
463
|
write(outcome.connected
|
|
254
|
-
? unstableNotice(opts.
|
|
255
|
-
: exhaustedNotice(opts.
|
|
464
|
+
? unstableNotice(opts.target, opts.host.name)
|
|
465
|
+
: exhaustedNotice(opts.target, opts.host.name));
|
|
256
466
|
}
|
|
257
467
|
else if (decision.code === REMOTE_EXIT_255_REMAPPED) {
|
|
258
|
-
write(remoteExitNotice(opts.
|
|
468
|
+
write(remoteExitNotice(opts.target, opts.host.name));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// Clean detach / agent exit after a reattach: the user is at a local
|
|
472
|
+
// shell and still needs the session id (RUSH-3227). Spent-budget and
|
|
473
|
+
// remapped-255 notices already carry recoveryHint.
|
|
474
|
+
write(connectionEndedNotice(opts.target, opts.host.name));
|
|
259
475
|
}
|
|
260
476
|
return decision.code;
|
|
261
477
|
}
|
|
262
|
-
write(reconnectNotice(opts.
|
|
263
|
-
await wait(decision.waitMs)
|
|
478
|
+
write(reconnectNotice(opts.target, opts.host.name, decision.state.attempt, decision.waitMs, decision.remainingMs));
|
|
479
|
+
if (await wait(decision.waitMs) === 'interrupted') {
|
|
480
|
+
// The user asked for their shell back. Nothing was lost — say where the
|
|
481
|
+
// agent is and how to return, then exit as an interrupt (128 + SIGINT).
|
|
482
|
+
write(interruptedNotice(opts.target, opts.host.name));
|
|
483
|
+
return 130;
|
|
484
|
+
}
|
|
264
485
|
state = decision.state;
|
|
265
|
-
outcome = reattach(opts.host, opts.
|
|
486
|
+
outcome = reattach(opts.host, opts.target);
|
|
266
487
|
}
|
|
267
488
|
}
|
|
@@ -27,6 +27,7 @@ import { resolveRemoteOsSync } from './remote-os.js';
|
|
|
27
27
|
import { loadDevices } from '../devices/registry.js';
|
|
28
28
|
import { resolveDeviceProfile } from '../devices/resolve-profile.js';
|
|
29
29
|
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
30
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../devices/interactive-host.js';
|
|
30
31
|
import { localMachineId } from '../session/origin-machine.js';
|
|
31
32
|
// Re-export so existing importers (tests, commands) keep their path; the class
|
|
32
33
|
// itself lives in types.ts so providers can throw it without a circular import.
|
|
@@ -161,6 +162,12 @@ export async function matchHost(name, opts = {}) {
|
|
|
161
162
|
// returning nothing — callers that already special-case "target is this
|
|
162
163
|
// machine" (teams add/create, the passthrough self-host check) then treat it as
|
|
163
164
|
// local exactly as they would if the user had typed the local name.
|
|
165
|
+
if (isDeviceInteractive(name)) {
|
|
166
|
+
const pinned = resolveInteractiveDevice();
|
|
167
|
+
if (!pinned)
|
|
168
|
+
throw new Error(interactiveUnsetError());
|
|
169
|
+
name = pinned;
|
|
170
|
+
}
|
|
164
171
|
if (isDeviceAuto(name)) {
|
|
165
172
|
const plan = (opts.resolveAuto ?? (() => resolveDeviceAffinity({})))();
|
|
166
173
|
const picked = plan.host ?? normalizeHost(localMachineId());
|
|
@@ -187,11 +187,10 @@ export declare function migrateWatchdogSentinelToConfig(sentinelPath?: string, e
|
|
|
187
187
|
*/
|
|
188
188
|
export declare function migrateCliDirToClis(agentsDirs: string[]): void;
|
|
189
189
|
/**
|
|
190
|
-
* Cursor's OAuth token
|
|
191
|
-
* ~/.config/cursor/auth.json
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* version home or it would read as logged out after upgrade. The active account
|
|
190
|
+
* An earlier agents-cli release put Cursor's file-backed OAuth token at
|
|
191
|
+
* ~/.config/cursor/auth.json. Current Cursor actually writes the file store to
|
|
192
|
+
* HOME-relative ~/.cursor/auth.json. Copy that legacy token into the active
|
|
193
|
+
* account's version home. The active account
|
|
195
194
|
* is the home the ~/.cursor symlink currently targets. Idempotent: skips when
|
|
196
195
|
* the home already has its own token, and a no-op for unmanaged Cursor installs
|
|
197
196
|
* (where ~/.cursor is a real dir, not a symlink into a version home).
|
|
@@ -1641,7 +1641,7 @@ function migrateVersionResourcesToPatterns() {
|
|
|
1641
1641
|
}
|
|
1642
1642
|
}
|
|
1643
1643
|
if (changed) {
|
|
1644
|
-
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/
|
|
1644
|
+
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/cli/schema/agents-yaml.schema.json\n\n';
|
|
1645
1645
|
fs.writeFileSync(metaFile, META_HEADER + yaml.stringify(meta), 'utf-8');
|
|
1646
1646
|
console.error('Migrated agents.yaml versions: entries to pattern format');
|
|
1647
1647
|
}
|
|
@@ -2278,11 +2278,10 @@ function migrateHumans() {
|
|
|
2278
2278
|
}
|
|
2279
2279
|
}
|
|
2280
2280
|
/**
|
|
2281
|
-
* Cursor's OAuth token
|
|
2282
|
-
* ~/.config/cursor/auth.json
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
* version home or it would read as logged out after upgrade. The active account
|
|
2281
|
+
* An earlier agents-cli release put Cursor's file-backed OAuth token at
|
|
2282
|
+
* ~/.config/cursor/auth.json. Current Cursor actually writes the file store to
|
|
2283
|
+
* HOME-relative ~/.cursor/auth.json. Copy that legacy token into the active
|
|
2284
|
+
* account's version home. The active account
|
|
2286
2285
|
* is the home the ~/.cursor symlink currently targets. Idempotent: skips when
|
|
2287
2286
|
* the home already has its own token, and a no-op for unmanaged Cursor installs
|
|
2288
2287
|
* (where ~/.cursor is a real dir, not a symlink into a version home).
|
|
@@ -2305,7 +2304,7 @@ export function seedActiveCursorLoginPerVersion() {
|
|
|
2305
2304
|
}
|
|
2306
2305
|
if (!versionHome.includes(path.join('versions', 'cursor')))
|
|
2307
2306
|
return;
|
|
2308
|
-
const dest = path.join(versionHome, '.
|
|
2307
|
+
const dest = path.join(versionHome, '.cursor', 'auth.json');
|
|
2309
2308
|
try {
|
|
2310
2309
|
if (fs.existsSync(dest))
|
|
2311
2310
|
return;
|
|
@@ -174,7 +174,7 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
174
174
|
* sorted alphabetically before the real self-updated grok binary and
|
|
175
175
|
* was silently launched instead.
|
|
176
176
|
*/
|
|
177
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
177
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 18;
|
|
178
178
|
/**
|
|
179
179
|
* Agents whose config directory can be relocated by an environment variable
|
|
180
180
|
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
@@ -942,7 +942,13 @@ export function removeShim(agent) {
|
|
|
942
942
|
* sorted alphabetically before the real self-updated grok binary and
|
|
943
943
|
* was silently launched instead.
|
|
944
944
|
*/
|
|
945
|
-
|
|
945
|
+
// v17 — the claude alias env now reuses the adapter's shimConfigEnvBash (adds the
|
|
946
|
+
// Linux .oauth_token setup-token fallback the hand-copied subset had dropped),
|
|
947
|
+
// so existing alias shims must regenerate to pick it up.
|
|
948
|
+
// v18 — Cursor aliases select the file credential store and swap HOME to the
|
|
949
|
+
// version home because current Cursor writes auth.json under ~/.cursor
|
|
950
|
+
// and ignores XDG_CONFIG_HOME for credential storage.
|
|
951
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 18;
|
|
946
952
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
947
953
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
948
954
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -990,14 +996,16 @@ export function generateVersionedAliasScript(agent, version) {
|
|
|
990
996
|
const configDirName = path.relative(os.homedir(), agentConfig.configDir);
|
|
991
997
|
const managedEnv = agent === 'claude'
|
|
992
998
|
? `
|
|
993
|
-
#
|
|
994
|
-
#
|
|
995
|
-
|
|
996
|
-
#
|
|
997
|
-
#
|
|
998
|
-
#
|
|
999
|
-
|
|
1000
|
-
|
|
999
|
+
# Reuse the main shim's Claude config-env verbatim (CLAUDE_CONFIG_DIR pin,
|
|
1000
|
+
# autoupdater off, AND the Linux .oauth_token setup-token fallback), keyed to this
|
|
1001
|
+
# version's home via VERSION_DIR. A hand-copied subset here had drifted and lost the
|
|
1002
|
+
# .oauth_token fallback, so interactive runs on a keychain-less worker could not
|
|
1003
|
+
# authenticate from the attached setup-token — the main shim (generateShimScript)
|
|
1004
|
+
# already sources this same adapter block, so sharing it keeps the two in sync.
|
|
1005
|
+
# plain (not exported) so it feeds shimConfigEnvBash below but never leaks into the
|
|
1006
|
+
# launched agent process — matching the main shim's convention.
|
|
1007
|
+
VERSION_DIR="$HOME/.agents/.history/versions/${agent}/${version}"
|
|
1008
|
+
${resolveHarnessAdapter(agent).shimConfigEnvBash?.({ configDirName }) ?? ''}`
|
|
1001
1009
|
: agent === 'codex'
|
|
1002
1010
|
? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
|
|
1003
1011
|
: agent === 'copilot'
|
|
@@ -1037,10 +1045,12 @@ export XDG_DATA_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.
|
|
|
1037
1045
|
`
|
|
1038
1046
|
: agent === 'cursor'
|
|
1039
1047
|
? `
|
|
1040
|
-
# Cursor
|
|
1041
|
-
#
|
|
1042
|
-
#
|
|
1043
|
-
export
|
|
1048
|
+
# Cursor defaults to one machine-global OS-keychain login on macOS. Force its
|
|
1049
|
+
# file store and swap HOME, so ~/.cursor/auth.json belongs to this
|
|
1050
|
+
# version and direct aliases cannot fall through to the shared keychain login.
|
|
1051
|
+
export AGENTS_REAL_HOME="\${AGENTS_REAL_HOME:-$HOME}"
|
|
1052
|
+
export HOME="$HOME/.agents/.history/versions/${agent}/${version}/home"
|
|
1053
|
+
export AGENT_CLI_CREDENTIAL_STORE="file"
|
|
1044
1054
|
`
|
|
1045
1055
|
: '';
|
|
1046
1056
|
const launchArgs = resolveHarnessAdapter(agent).shimLaunchArgs?.() ?? '';
|