@phnx-labs/agents-cli 1.22.35 → 1.22.38
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 +708 -2
- package/README.md +83 -43
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.d.ts +15 -0
- package/dist/bootstrap.js +1212 -0
- package/dist/commands/accounts.d.ts +4 -11
- package/dist/commands/accounts.js +159 -27
- package/dist/commands/artifacts-setup.d.ts +53 -0
- package/dist/commands/artifacts-setup.js +161 -0
- package/dist/commands/artifacts.d.ts +18 -0
- package/dist/commands/artifacts.js +58 -0
- package/dist/commands/attach.js +17 -4
- package/dist/commands/audit.d.ts +8 -8
- package/dist/commands/audit.js +57 -47
- package/dist/commands/browser.js +17 -11
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +0 -11
- package/dist/commands/computer-actions.js +33 -0
- package/dist/commands/computer-sessions-picker.d.ts +21 -0
- package/dist/commands/computer-sessions-picker.js +141 -0
- package/dist/commands/computer.d.ts +16 -0
- package/dist/commands/computer.js +37 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +401 -0
- package/dist/commands/cp.d.ts +84 -0
- package/dist/commands/cp.js +231 -0
- package/dist/commands/daemon.js +243 -14
- package/dist/commands/detach-core.d.ts +1 -1
- package/dist/commands/detach-core.js +12 -2
- package/dist/commands/detach.js +1 -1
- package/dist/commands/doctor.js +64 -5
- package/dist/commands/events.d.ts +24 -12
- package/dist/commands/events.js +144 -28
- package/dist/commands/exec.js +86 -42
- package/dist/commands/feed.d.ts +1 -1
- package/dist/commands/feed.js +16 -8
- package/dist/commands/focus.d.ts +33 -1
- package/dist/commands/focus.js +177 -17
- package/dist/commands/funnel.d.ts +1 -1
- package/dist/commands/go.js +9 -8
- package/dist/commands/harness-wizard.d.ts +1 -0
- package/dist/commands/harness-wizard.js +18 -20
- package/dist/commands/harness.d.ts +2 -0
- package/dist/commands/harness.js +59 -26
- package/dist/commands/hooks.js +0 -11
- package/dist/commands/inspect.d.ts +94 -1
- package/dist/commands/inspect.js +866 -40
- package/dist/commands/lease.d.ts +4 -4
- package/dist/commands/lease.js +6 -6
- package/dist/commands/login.js +0 -13
- package/dist/commands/logs.d.ts +2 -2
- package/dist/commands/logs.js +2 -2
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/memory.js +7 -2
- package/dist/commands/message.d.ts +1 -1
- package/dist/commands/message.js +37 -6
- package/dist/commands/models.js +71 -0
- package/dist/commands/monitors.d.ts +0 -1
- package/dist/commands/monitors.js +199 -16
- package/dist/commands/open.d.ts +18 -0
- package/dist/commands/open.js +57 -0
- package/dist/commands/packages.js +2 -107
- package/dist/commands/plugins.js +6 -3
- package/dist/commands/profiles.d.ts +1 -0
- package/dist/commands/profiles.js +16 -160
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +125 -5
- package/dist/commands/reconnect.js +9 -3
- package/dist/commands/repo.js +42 -1
- package/dist/commands/resource-view.d.ts +17 -0
- package/dist/commands/resource-view.js +36 -14
- package/dist/commands/resume.js +9 -2
- package/dist/commands/routines.d.ts +0 -2
- package/dist/commands/routines.js +473 -289
- package/dist/commands/secrets.js +52 -9
- package/dist/commands/sessions-export.d.ts +47 -0
- package/dist/commands/sessions-export.js +116 -6
- package/dist/commands/sessions-import.d.ts +17 -0
- package/dist/commands/sessions-import.js +114 -6
- package/dist/commands/sessions-migrate.d.ts +27 -0
- package/dist/commands/sessions-migrate.js +50 -9
- package/dist/commands/sessions-picker.js +23 -4
- package/dist/commands/sessions-resume.d.ts +5 -3
- package/dist/commands/sessions-resume.js +30 -8
- package/dist/commands/sessions-watch.d.ts +2 -0
- package/dist/commands/sessions-watch.js +53 -0
- package/dist/commands/sessions.d.ts +66 -3
- package/dist/commands/sessions.js +298 -47
- package/dist/commands/setup-browser.js +2 -2
- package/dist/commands/setup-mine.js +0 -1
- package/dist/commands/setup-preferences.js +1 -1
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +31 -5
- package/dist/commands/share.d.ts +87 -7
- package/dist/commands/share.js +320 -58
- package/dist/commands/skills.js +0 -11
- package/dist/commands/snapshot.js +1 -1
- package/dist/commands/ssh.d.ts +18 -0
- package/dist/commands/ssh.js +549 -210
- package/dist/commands/sync.d.ts +5 -0
- package/dist/commands/sync.js +212 -32
- package/dist/commands/teams.d.ts +13 -0
- package/dist/commands/teams.js +187 -30
- package/dist/commands/tickets.d.ts +2 -0
- package/dist/commands/tickets.js +43 -0
- package/dist/commands/update.js +2 -4
- package/dist/commands/usage.js +5 -3
- package/dist/commands/versions.js +11 -5
- package/dist/commands/view.js +15 -7
- package/dist/commands/webhook.d.ts +2 -2
- package/dist/commands/webhook.js +7 -7
- package/dist/index.d.ts +18 -2
- package/dist/index.js +53 -1264
- package/dist/lib/account-catalog.d.ts +18 -0
- package/dist/lib/account-catalog.js +38 -0
- package/dist/lib/account-provider-registry.d.ts +18 -0
- package/dist/lib/account-provider-registry.js +59 -0
- package/dist/lib/account-registry.d.ts +40 -0
- package/dist/lib/account-registry.js +239 -0
- package/dist/lib/account-schema.d.ts +44 -0
- package/dist/lib/account-schema.js +91 -0
- package/dist/lib/account-state-service.d.ts +21 -0
- package/dist/lib/account-state-service.js +60 -0
- package/dist/lib/activity.d.ts +1 -1
- package/dist/lib/activity.js +8 -1
- package/dist/lib/add-dir.d.ts +80 -0
- package/dist/lib/add-dir.js +241 -0
- package/dist/lib/agent-cli-commands.d.ts +11 -0
- package/dist/lib/agent-cli-commands.js +30 -0
- package/dist/lib/agents.js +44 -36
- package/dist/lib/answer-router.js +1 -1
- package/dist/lib/audit/log.d.ts +10 -27
- package/dist/lib/audit/log.js +20 -33
- package/dist/lib/auth-health.d.ts +9 -2
- package/dist/lib/auth-health.js +42 -9
- package/dist/lib/auto-pull-worker.js +7 -3
- package/dist/lib/auto-pull.d.ts +44 -3
- package/dist/lib/auto-pull.js +88 -5
- package/dist/lib/binary-shadow.d.ts +28 -0
- package/dist/lib/binary-shadow.js +121 -0
- package/dist/lib/brand.js +9 -3
- package/dist/lib/browser/hygiene.d.ts +90 -0
- package/dist/lib/browser/hygiene.js +146 -0
- package/dist/lib/browser/ipc.d.ts +30 -0
- package/dist/lib/browser/ipc.js +93 -4
- package/dist/lib/browser/profiles.d.ts +3 -2
- package/dist/lib/browser/profiles.js +65 -22
- package/dist/lib/browser/service.d.ts +79 -1
- package/dist/lib/browser/service.js +231 -14
- package/dist/lib/browser/sessions-list.d.ts +22 -2
- package/dist/lib/browser/sessions-list.js +54 -6
- package/dist/lib/browser/stream.d.ts +2 -0
- package/dist/lib/browser/stream.js +1 -0
- package/dist/lib/browser/types.d.ts +63 -1
- package/dist/lib/byok-usage.d.ts +9 -2
- package/dist/lib/byok-usage.js +133 -31
- package/dist/lib/cli-entry.d.ts +11 -0
- package/dist/lib/cli-entry.js +46 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +2 -2
- package/dist/lib/cloud/registry.js +1 -1
- package/dist/lib/cloud/types.d.ts +1 -1
- package/dist/lib/commands.d.ts +2 -0
- package/dist/lib/commands.js +2 -0
- package/dist/lib/computer/sessions-list.d.ts +150 -0
- package/dist/lib/computer/sessions-list.js +363 -0
- package/dist/lib/config-keys.d.ts +86 -0
- package/dist/lib/config-keys.js +193 -0
- package/dist/lib/config-machine-keys.d.ts +12 -0
- package/dist/lib/config-machine-keys.js +17 -0
- package/dist/lib/config-transfer.js +3 -4
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/cli.js +3 -3
- package/dist/lib/daemon-health.d.ts +22 -0
- package/dist/lib/daemon-health.js +33 -3
- package/dist/lib/daemon-services.d.ts +50 -0
- package/dist/lib/daemon-services.js +147 -0
- package/dist/lib/daemon-ticks.d.ts +35 -66
- package/dist/lib/daemon-ticks.js +77 -159
- package/dist/lib/daemon.d.ts +72 -4
- package/dist/lib/daemon.js +792 -202
- package/dist/lib/deeplink/register.d.ts +49 -0
- package/dist/lib/deeplink/register.js +253 -0
- package/dist/lib/deeplink/url.d.ts +34 -0
- package/dist/lib/deeplink/url.js +78 -0
- package/dist/lib/device-config.d.ts +124 -29
- package/dist/lib/device-config.js +346 -141
- package/dist/lib/devices/config-migration.d.ts +42 -0
- package/dist/lib/devices/config-migration.js +203 -0
- package/dist/lib/devices/connect.js +14 -7
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +20 -1
- package/dist/lib/devices/harness-inventory.d.ts +11 -2
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/health.d.ts +1 -1
- package/dist/lib/devices/health.js +1 -1
- package/dist/lib/devices/pending.d.ts +22 -1
- package/dist/lib/devices/pending.js +67 -2
- package/dist/lib/devices/registry.d.ts +0 -25
- package/dist/lib/devices/registry.js +2 -83
- package/dist/lib/devices/resolve-profile.d.ts +20 -0
- package/dist/lib/devices/resolve-profile.js +44 -0
- package/dist/lib/devices/rollout-verify.d.ts +101 -0
- package/dist/lib/devices/rollout-verify.js +193 -0
- package/dist/lib/devices/ssh-config.js +10 -6
- package/dist/lib/event-families.d.ts +28 -0
- package/dist/lib/event-families.js +159 -0
- package/dist/lib/event-stream.d.ts +9 -1
- package/dist/lib/event-stream.js +26 -9
- package/dist/lib/events.d.ts +9 -1
- package/dist/lib/events.js +21 -6
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +219 -121
- package/dist/lib/feed-broadcast.d.ts +25 -0
- package/dist/lib/feed-broadcast.js +33 -0
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +12 -2
- package/dist/lib/fleet/capture.js +15 -0
- package/dist/lib/fleet/manifest.js +9 -0
- package/dist/lib/fleet/types.d.ts +9 -0
- package/dist/lib/fleet-status.js +10 -8
- package/dist/lib/git.d.ts +21 -7
- package/dist/lib/git.js +122 -18
- package/dist/lib/hosts/dispatch.d.ts +8 -3
- package/dist/lib/hosts/dispatch.js +29 -3
- package/dist/lib/hosts/logs.d.ts +2 -2
- package/dist/lib/hosts/logs.js +2 -2
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.d.ts +2 -3
- package/dist/lib/hosts/passthrough.js +10 -24
- package/dist/lib/hosts/progress.js +2 -2
- package/dist/lib/hosts/providers/devices.d.ts +3 -3
- package/dist/lib/hosts/providers/devices.js +18 -12
- package/dist/lib/hosts/ready.d.ts +5 -1
- package/dist/lib/hosts/ready.js +9 -3
- package/dist/lib/hosts/reconcile.d.ts +2 -2
- package/dist/lib/hosts/reconcile.js +2 -2
- package/dist/lib/hosts/registry.js +14 -9
- package/dist/lib/hosts/remote-cmd.js +1 -1
- package/dist/lib/hosts/remote-os.d.ts +9 -6
- package/dist/lib/hosts/remote-os.js +13 -6
- package/dist/lib/hosts/routing-flag.d.ts +23 -0
- package/dist/lib/hosts/routing-flag.js +41 -0
- package/dist/lib/hosts/run-target.js +1 -1
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/hosts/tasks.d.ts +6 -6
- package/dist/lib/hosts/tasks.js +4 -4
- package/dist/lib/hosts/types.js +1 -2
- package/dist/lib/installations/resolve.js +2 -8
- package/dist/lib/isolation-boundary-report.js +0 -1
- package/dist/lib/linear-cache.d.ts +7 -0
- package/dist/lib/linear-cache.js +31 -4
- package/dist/lib/linear-project-counts.d.ts +1 -1
- package/dist/lib/linear-project-counts.js +2 -3
- package/dist/lib/linear-projects.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/mailbox-target.d.ts +1 -1
- package/dist/lib/mailbox-target.js +1 -1
- package/dist/lib/manifest.js +7 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +3 -2
- package/dist/lib/menubar/snapshot.js +6 -2
- package/dist/lib/migrate-fold.d.ts +33 -0
- package/dist/lib/migrate-fold.js +130 -0
- package/dist/lib/migrate.d.ts +34 -34
- package/dist/lib/migrate.js +106 -128
- package/dist/lib/model-tier-overrides.d.ts +1 -1
- package/dist/lib/model-tier-overrides.js +15 -2
- package/dist/lib/monitors/config.d.ts +17 -3
- package/dist/lib/monitors/config.js +52 -18
- package/dist/lib/monitors/engine.d.ts +32 -1
- package/dist/lib/monitors/engine.js +94 -11
- package/dist/lib/monitors/fingerprint.d.ts +46 -0
- package/dist/lib/monitors/fingerprint.js +100 -0
- package/dist/lib/monitors/remote.d.ts +47 -0
- package/dist/lib/monitors/remote.js +81 -0
- package/dist/lib/monitors/state.d.ts +39 -0
- package/dist/lib/monitors/state.js +56 -0
- package/dist/lib/onepassword.d.ts +17 -0
- package/dist/lib/onepassword.js +52 -22
- package/dist/lib/open-url.js +1 -1
- package/dist/lib/picker.d.ts +14 -2
- package/dist/lib/picker.js +35 -13
- package/dist/lib/placement.d.ts +1 -1
- package/dist/lib/placement.js +1 -1
- package/dist/lib/pr-land-detach.d.ts +32 -0
- package/dist/lib/pr-land-detach.js +79 -0
- package/dist/lib/profiles-presets.js +13 -0
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +44 -1
- package/dist/lib/project-focus.d.ts +1 -1
- package/dist/lib/project-focus.js +1 -1
- package/dist/lib/project-probe.d.ts +4 -1
- package/dist/lib/project-probe.js +5 -4
- package/dist/lib/project-resources.js +2 -18
- package/dist/lib/project-root.d.ts +16 -0
- package/dist/lib/project-root.js +20 -2
- package/dist/lib/projects.d.ts +20 -1
- package/dist/lib/projects.js +78 -1
- package/dist/lib/refresh-coordinator.d.ts +24 -0
- package/dist/lib/refresh-coordinator.js +52 -0
- package/dist/lib/registry.d.ts +2 -2
- package/dist/lib/registry.js +1 -1
- package/dist/lib/resource-aliases.d.ts +13 -0
- package/dist/lib/resource-aliases.js +26 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +66 -10
- package/dist/lib/rotate.d.ts +3 -4
- package/dist/lib/rotate.js +4 -5
- package/dist/lib/routine-activation.d.ts +22 -2
- package/dist/lib/routine-activation.js +63 -16
- package/dist/lib/routine-context.d.ts +9 -2
- package/dist/lib/routine-context.js +21 -6
- package/dist/lib/routines-project.d.ts +58 -48
- package/dist/lib/routines-project.js +190 -159
- package/dist/lib/routines.d.ts +44 -7
- package/dist/lib/routines.js +92 -34
- package/dist/lib/run-defaults.d.ts +5 -0
- package/dist/lib/run-defaults.js +11 -1
- package/dist/lib/runner.d.ts +10 -8
- package/dist/lib/runner.js +122 -39
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +10 -3
- package/dist/lib/secrets/agent.js +43 -8
- package/dist/lib/secrets/bundles.d.ts +1 -0
- package/dist/lib/secrets/bundles.js +92 -80
- package/dist/lib/secrets/index.js +32 -9
- package/dist/lib/secrets/install-helper.d.ts +4 -4
- package/dist/lib/secrets/install-helper.js +4 -4
- package/dist/lib/secrets/push.d.ts +40 -6
- package/dist/lib/secrets/push.js +58 -11
- package/dist/lib/secrets/read-backoff.d.ts +1 -1
- package/dist/lib/secrets/read-backoff.js +1 -1
- package/dist/lib/secrets/remote.d.ts +24 -0
- package/dist/lib/secrets/remote.js +54 -4
- package/dist/lib/self-update.d.ts +108 -0
- package/dist/lib/self-update.js +276 -1
- package/dist/lib/session/active.d.ts +89 -5
- package/dist/lib/session/active.js +165 -10
- package/dist/lib/session/actor-sidecar.d.ts +1 -1
- package/dist/lib/session/actor-sidecar.js +2 -1
- package/dist/lib/session/db.d.ts +176 -2
- package/dist/lib/session/db.js +484 -17
- package/dist/lib/session/discover.js +27 -4
- package/dist/lib/session/hook-sessions.js +2 -2
- package/dist/lib/session/host-link.d.ts +2 -2
- package/dist/lib/session/host-link.js +2 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +57 -0
- package/dist/lib/session/remote-active.js +15 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/session-cache.d.ts +14 -7
- package/dist/lib/session/session-cache.js +51 -9
- package/dist/lib/session/state.d.ts +2 -2
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.d.ts +10 -10
- package/dist/lib/session/sync/config.js +10 -10
- package/dist/lib/session/sync/r2.d.ts +34 -0
- package/dist/lib/session/sync/r2.js +123 -0
- package/dist/lib/session/throughput.d.ts +1 -1
- package/dist/lib/session/throughput.js +1 -1
- package/dist/lib/session/types.d.ts +23 -1
- package/dist/lib/session/types.js +15 -0
- package/dist/lib/session/viewing-in.d.ts +1 -1
- package/dist/lib/session/viewing-in.js +1 -1
- package/dist/lib/session/watch.d.ts +101 -0
- package/dist/lib/session/watch.js +242 -0
- package/dist/lib/share/analytics.js +1 -1
- package/dist/lib/share/capture.d.ts +1 -1
- package/dist/lib/share/capture.js +3 -3
- package/dist/lib/share/config.d.ts +6 -2
- package/dist/lib/share/config.js +6 -5
- package/dist/lib/share/delete.js +3 -3
- package/dist/lib/share/provision.d.ts +38 -0
- package/dist/lib/share/provision.js +51 -1
- package/dist/lib/share/publish.d.ts +40 -1
- package/dist/lib/share/publish.js +119 -6
- package/dist/lib/share/worker-template.d.ts +12 -1
- package/dist/lib/share/worker-template.js +83 -7
- package/dist/lib/shims.d.ts +17 -3
- package/dist/lib/shims.js +98 -26
- package/dist/lib/signin-badge.js +3 -2
- package/dist/lib/skills.js +2 -0
- package/dist/lib/smart-launch.d.ts +8 -8
- package/dist/lib/smart-launch.js +80 -59
- package/dist/lib/ssh-exec.d.ts +1 -1
- package/dist/lib/ssh-exec.js +1 -1
- package/dist/lib/staleness/prune.d.ts +88 -0
- package/dist/lib/staleness/prune.js +69 -0
- package/dist/lib/staleness/registry.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.js +34 -2
- package/dist/lib/staleness/writers/hooks.js +16 -0
- package/dist/lib/staleness/writers/kinds.d.ts +6 -0
- package/dist/lib/staleness/writers/skills.js +19 -0
- package/dist/lib/staleness/writers/subagents.d.ts +2 -3
- package/dist/lib/staleness/writers/subagents.js +0 -4
- package/dist/lib/staleness/writers/types.d.ts +32 -0
- package/dist/lib/startup/command-registry.d.ts +16 -14
- package/dist/lib/startup/command-registry.js +26 -37
- package/dist/lib/startup/dev-build.d.ts +10 -0
- package/dist/lib/startup/dev-build.js +13 -1
- package/dist/lib/startup/spellcheck.d.ts +18 -0
- package/dist/lib/startup/spellcheck.js +45 -0
- package/dist/lib/state.d.ts +36 -9
- package/dist/lib/state.js +96 -46
- package/dist/lib/subagents-registry.d.ts +0 -7
- package/dist/lib/subagents-registry.js +38 -65
- package/dist/lib/subagents.d.ts +0 -33
- package/dist/lib/subagents.js +0 -75
- package/dist/lib/sync-umbrella.js +1 -1
- package/dist/lib/teams/agents.d.ts +41 -1
- package/dist/lib/teams/agents.js +187 -9
- package/dist/lib/teams/api.d.ts +11 -1
- package/dist/lib/teams/api.js +11 -2
- package/dist/lib/teams/delivery.d.ts +41 -0
- package/dist/lib/teams/delivery.js +60 -0
- package/dist/lib/teams/registry.d.ts +14 -0
- package/dist/lib/teams/registry.js +51 -1
- package/dist/lib/teams/remoteWorktree.d.ts +19 -0
- package/dist/lib/teams/remoteWorktree.js +29 -0
- package/dist/lib/teams/scheduler.d.ts +1 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/teams/worktree.d.ts +24 -0
- package/dist/lib/teams/worktree.js +42 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
- package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
- package/dist/lib/terminal/engine.d.ts +4 -0
- package/dist/lib/terminal/engine.js +8 -2
- package/dist/lib/terminal/types.d.ts +21 -2
- package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
- package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
- package/dist/lib/tickets/list.d.ts +53 -0
- package/dist/lib/tickets/list.js +153 -0
- package/dist/lib/tmux/orphan-reap.d.ts +316 -0
- package/dist/lib/tmux/orphan-reap.js +638 -0
- package/dist/lib/tmux/session.d.ts +68 -3
- package/dist/lib/tmux/session.js +96 -5
- package/dist/lib/triggers/handlers.d.ts +20 -0
- package/dist/lib/triggers/handlers.js +16 -1
- package/dist/lib/triggers/webhook.js +10 -0
- package/dist/lib/types.d.ts +47 -35
- package/dist/lib/usage-fleet.d.ts +32 -0
- package/dist/lib/usage-fleet.js +125 -0
- package/dist/lib/usage-refresh.d.ts +3 -3
- package/dist/lib/usage-refresh.js +19 -16
- package/dist/lib/usage.d.ts +51 -47
- package/dist/lib/usage.js +145 -153
- package/dist/lib/version-duplicates.js +2 -2
- package/dist/lib/versions.d.ts +56 -13
- package/dist/lib/versions.js +195 -45
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +3 -3
- package/dist/lib/watchdog/rotate.d.ts +6 -6
- package/dist/lib/watchdog/rotate.js +6 -6
- package/dist/lib/workflows.d.ts +1 -1
- package/dist/lib/yaml-io.d.ts +47 -0
- package/dist/lib/yaml-io.js +55 -0
- package/package.json +2 -1
- package/scripts/install-helper.js +2 -2
- package/scripts/postinstall.js +23 -20
- package/dist/commands/defaults.d.ts +0 -7
- package/dist/commands/defaults.js +0 -107
- package/dist/commands/export.d.ts +0 -11
- package/dist/commands/export.js +0 -215
- package/dist/commands/helper.d.ts +0 -12
- package/dist/commands/helper.js +0 -87
- package/dist/commands/hosts.d.ts +0 -11
- package/dist/commands/hosts.js +0 -330
- package/dist/commands/lock.d.ts +0 -12
- package/dist/commands/lock.js +0 -70
- package/dist/commands/pull.d.ts +0 -17
- package/dist/commands/pull.js +0 -39
- package/dist/commands/push.d.ts +0 -14
- package/dist/commands/push.js +0 -30
- package/dist/commands/set.d.ts +0 -15
- package/dist/commands/set.js +0 -79
- package/dist/commands/setup-share.d.ts +0 -17
- package/dist/commands/setup-share.js +0 -85
- package/dist/commands/wallet.d.ts +0 -20
- package/dist/commands/wallet.js +0 -216
- package/dist/commands/worktree.d.ts +0 -19
- package/dist/commands/worktree.js +0 -272
- package/dist/lib/account-labels.d.ts +0 -24
- package/dist/lib/account-labels.js +0 -72
- package/dist/lib/auto-dispatch-linear.d.ts +0 -18
- package/dist/lib/auto-dispatch-linear.js +0 -107
- package/dist/lib/auto-dispatch-provider.d.ts +0 -10
- package/dist/lib/auto-dispatch-provider.js +0 -30
- package/dist/lib/auto-dispatch.d.ts +0 -93
- package/dist/lib/auto-dispatch.js +0 -128
- package/dist/lib/export.d.ts +0 -72
- package/dist/lib/export.js +0 -269
- package/dist/lib/lock.d.ts +0 -93
- package/dist/lib/lock.js +0 -207
- package/dist/lib/wallet/index.d.ts +0 -78
- package/dist/lib/wallet/index.js +0 -253
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reap the helper processes an exited agent leaves behind (RUSH-2521).
|
|
3
|
+
*
|
|
4
|
+
* ## The leak
|
|
5
|
+
*
|
|
6
|
+
* An interactive agent runs as the leaf of a detached tmux pane
|
|
7
|
+
* (`runInTmux` → `createSession`, [`../exec.ts`](../exec.ts)). Anything the
|
|
8
|
+
* harness spawns — MCP servers, background daemons, `exec` helper trees — is a
|
|
9
|
+
* child of that pane. When the agent exits, the kernel SIGHUPs the pane's
|
|
10
|
+
* FOREGROUND process group only. A helper that put itself in its own process
|
|
11
|
+
* group, or that ignores SIGHUP, survives: it reparents to init and keeps its
|
|
12
|
+
* memory forever. Measured on the fleet: one pane holding 2.5 GB of Claude Code
|
|
13
|
+
* background daemons 22 days after its session ended, and 34 orphaned
|
|
14
|
+
* `cgraph-mcp --daemon` processes on a single worker.
|
|
15
|
+
*
|
|
16
|
+
* ## Why attribution is by ENVIRONMENT, not by pid ancestry / tty / pgid
|
|
17
|
+
*
|
|
18
|
+
* Every obvious OS handle is destroyed by exactly the event we are detecting:
|
|
19
|
+
*
|
|
20
|
+
* - **ppid ancestry** — an orphan is reparented to init the instant its parent
|
|
21
|
+
* dies, so the chain back to the pane is gone.
|
|
22
|
+
* - **controlling terminal** — POSIX disassociates the controlling terminal from
|
|
23
|
+
* every process in a session when the session leader exits, so the orphan's
|
|
24
|
+
* `tty` reads `?`. Verified on Linux: an orphan under a dead pane leader shows
|
|
25
|
+
* `tty=?` while its own pane still reports `pane_tty=/dev/pts/6`.
|
|
26
|
+
* - **process group** — the helpers that survive are precisely the ones that
|
|
27
|
+
* left the pane's process group (that is why they survived the SIGHUP).
|
|
28
|
+
* - **POSIX session id** — durable and correct, but not portable: `ps -o sid=`
|
|
29
|
+
* exists on Linux and does not exist on macOS (`ps: sid: keyword not found`),
|
|
30
|
+
* `ps -o sess=` reports 0 there, and `pgrep -s` is Linux-only.
|
|
31
|
+
*
|
|
32
|
+
* What DOES survive all of it is the process environment. `runInTmux` stamps
|
|
33
|
+
* `AGENT_TMUX_SESSION_NAME` into the pane env ([`../exec.ts`](../exec.ts)), and
|
|
34
|
+
* every descendant inherits it — including a reparented one. So a process
|
|
35
|
+
* carrying `AGENT_TMUX_SESSION_NAME=<name>` is attributable to that tmux session
|
|
36
|
+
* with no heuristics at all, and tmux itself is the liveness oracle for whether
|
|
37
|
+
* that session's agent is still running.
|
|
38
|
+
*
|
|
39
|
+
* ## The second tier: harness daemons that scrub the environment
|
|
40
|
+
*
|
|
41
|
+
* Claude Code's background pool (`claude … daemon run`, which owns the
|
|
42
|
+
* `bg-pty-host` / `bg-spare` processes) starts with a clean environment, so tier
|
|
43
|
+
* one cannot see it. It does, however, declare its owner in its own argv:
|
|
44
|
+
* `daemon run --origin transient --spawned-by {"label":"claude",…,"pid":3834601}`.
|
|
45
|
+
* A pool whose declared spawner pid is dead is unowned, so it and its descendants
|
|
46
|
+
* are reaped. That rule lives in {@link DETACHED_HELPER_RULES} rather than an
|
|
47
|
+
* inline `if (agent === 'claude')`, so another harness that detaches helpers the
|
|
48
|
+
* same way is one table entry.
|
|
49
|
+
*
|
|
50
|
+
* ## Safety
|
|
51
|
+
*
|
|
52
|
+
* Nothing is killed without positive proof its owner is gone:
|
|
53
|
+
*
|
|
54
|
+
* - a tmux session that still exists AND whose agent pane process is alive
|
|
55
|
+
* protects every process it owns;
|
|
56
|
+
* - a tmux session with a client attached protects every process it owns,
|
|
57
|
+
* whatever the pane state;
|
|
58
|
+
* - the reaping process itself, its ancestors, pid 1, and every long-lived
|
|
59
|
+
* agents-cli service ({@link isProtectedAgentsService}) are never candidates.
|
|
60
|
+
* - an UNRELIABLE read of tmux's session state (the query threw, timed out, or
|
|
61
|
+
* tmux itself never answered) disables tier 1 for that sweep entirely;
|
|
62
|
+
* - an absent session entry is unknown, even after a reliable read. A tmux
|
|
63
|
+
* server restart can produce the same empty snapshot while marked agents
|
|
64
|
+
* remain alive, so tier 1 requires a present owner with positive death proof.
|
|
65
|
+
* - tier 2 is anchored on the actual claude EXECUTABLE (argv[0]'s basename),
|
|
66
|
+
* never a substring match anywhere in the command line, and excludes any
|
|
67
|
+
* process still structurally part of a LIVE pane leaf's process tree right
|
|
68
|
+
* now (tmux's own `#{pane_pid}`, expanded to its current descendants —
|
|
69
|
+
* independent of whether any of those processes' environment markers were
|
|
70
|
+
* ever readable) or that carries a marker whose own session is
|
|
71
|
+
* live/attached — a live interactive agent, OR a subprocess it spawns
|
|
72
|
+
* (e.g. its own Bash tool invoking `claude --print "…"` as a sub-task),
|
|
73
|
+
* whose argv happens to quote this file's docblock (it contains the exact
|
|
74
|
+
* `daemon run --spawned-by {"pid":...}` shape) must never become a kill
|
|
75
|
+
* seed, and that has to hold even where tier 1's marker is dead (macOS, or
|
|
76
|
+
* any `claude` invocation started outside an agents-cli pane). Two review
|
|
77
|
+
* rounds found this: first that marker-only exclusion left the whole
|
|
78
|
+
* macOS/bare-invocation case open (round 2, closed by the pane-pid check),
|
|
79
|
+
* then that the pane-pid check alone only covered the pane LEAF itself, not
|
|
80
|
+
* its own live children (round 3, closed by expanding the check to the
|
|
81
|
+
* leaf's current descendant set via {@link descendantsOf}). A process that
|
|
82
|
+
* has genuinely reparented away — a real detached daemon, no longer chained
|
|
83
|
+
* to any live leaf by ppid — is deliberately NOT in that set and remains
|
|
84
|
+
* reapable, whatever unrelated live agent happens to also be running.
|
|
85
|
+
*
|
|
86
|
+
* ## Known platform gap — tier 1 is Linux-only today
|
|
87
|
+
*
|
|
88
|
+
* Tier 1's env-marker attribution requires reading another process's
|
|
89
|
+
* environment. `/proc/<pid>/environ` makes that a plain file read on Linux.
|
|
90
|
+
* macOS has no `/proc`; `ps -E` was the fallback, but on modern macOS (verified
|
|
91
|
+
* live on mac-mini 15.6, macOS Sequoia) `ps -E` no longer prints another
|
|
92
|
+
* process's environment at all — only the CALLING process's own env shows up.
|
|
93
|
+
* `readAgentProcesses()` therefore returns every macOS process with
|
|
94
|
+
* `tmuxSession: undefined`, and tier 1's `if (!p.tmuxSession) continue` skips
|
|
95
|
+
* all of them. This is SAFE (tier 1 silently no-ops rather than misattributing
|
|
96
|
+
* anything) but not effective: on macOS, only tier 2 — the detached-helper
|
|
97
|
+
* registry — can reap anything today. A macOS fix needs a different channel
|
|
98
|
+
* entirely (env is inherently unreadable cross-process there without a native
|
|
99
|
+
* helper); it is not a parsing bug {@link parseTmuxSessionMarker} can fix.
|
|
100
|
+
*/
|
|
101
|
+
import { execFile } from 'child_process';
|
|
102
|
+
import * as fs from 'fs';
|
|
103
|
+
import { promisify } from 'util';
|
|
104
|
+
const execFileAsync = promisify(execFile);
|
|
105
|
+
/** Ceiling on the `ps` snapshot so a wedged `ps` can never stall the daemon tick. */
|
|
106
|
+
const PS_TIMEOUT_MS = 10_000;
|
|
107
|
+
/**
|
|
108
|
+
* Ceiling on a single tmux pane-owner query. Without this a wedged tmux server
|
|
109
|
+
* hangs `readPaneOwners` forever, which — before this bound existed — could
|
|
110
|
+
* only be told apart from "genuinely no sessions" by the caller giving up and
|
|
111
|
+
* treating it as empty (RUSH-2521 review: exactly the failure mode that let a
|
|
112
|
+
* flaky tick select every marked process on the box as orphaned).
|
|
113
|
+
*/
|
|
114
|
+
const TMUX_QUERY_TIMEOUT_MS = 10_000;
|
|
115
|
+
/** Grace between SIGTERM and SIGKILL for a reaped orphan. */
|
|
116
|
+
export const REAP_GRACE_MS = 2_000;
|
|
117
|
+
/** Env var `runInTmux` stamps into the pane, inherited by every descendant. */
|
|
118
|
+
export const TMUX_SESSION_ENV = 'AGENT_TMUX_SESSION_NAME';
|
|
119
|
+
/**
|
|
120
|
+
* Basename of argv[0] — the actual executable a process runs, not whatever
|
|
121
|
+
* text later argv tokens happen to contain. `ps -o args=` gives one opaque
|
|
122
|
+
* command-line string (no reliable per-token quoting to split on), so this
|
|
123
|
+
* takes the first whitespace-delimited run and strips its directory prefix.
|
|
124
|
+
*/
|
|
125
|
+
function argv0Basename(args) {
|
|
126
|
+
const m = /^\s*(\S+)/.exec(args);
|
|
127
|
+
const token = m ? m[1] : '';
|
|
128
|
+
const base = token.split(/[\\/]/).pop() ?? '';
|
|
129
|
+
return base.toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Claude Code's background daemon pool. Its argv carries a JSON `--spawned-by`
|
|
133
|
+
* blob naming the claude process that started it, e.g.
|
|
134
|
+
* `claude.exe daemon run --origin transient --spawned-by {"label":"claude","cwd":"/…","pid":3834601}`.
|
|
135
|
+
* The `bg-pty-host` / `bg-spare` workers are its children and are reaped as
|
|
136
|
+
* descendants rather than matched individually — they carry no owner of their own.
|
|
137
|
+
*
|
|
138
|
+
* Anchored on argv[0]'s basename, not a substring match anywhere in the full
|
|
139
|
+
* command line: `daemon\s+run` and `--spawned-by\s+.*"pid"\s*:\s*(\d+)` both
|
|
140
|
+
* appear verbatim in THIS FILE's own docblock above, so a `grep`, `cat`, or
|
|
141
|
+
* pager viewing `orphan-reap.ts` — or a coding agent whose tool-call argv
|
|
142
|
+
* quotes this review's own bug report — matched the old substring-only rule
|
|
143
|
+
* and became a kill seed for its whole process subtree (RUSH-2521 review,
|
|
144
|
+
* reproduced against the exact diff text). Only a process whose real
|
|
145
|
+
* executable is `claude`/`claude.exe` can seed this rule now.
|
|
146
|
+
*/
|
|
147
|
+
const CLAUDE_BG_DAEMON = {
|
|
148
|
+
agent: 'claude',
|
|
149
|
+
spawnerPid(args) {
|
|
150
|
+
const exe = argv0Basename(args);
|
|
151
|
+
if (exe !== 'claude' && exe !== 'claude.exe')
|
|
152
|
+
return undefined;
|
|
153
|
+
if (!/\bdaemon\s+run\b/.test(args))
|
|
154
|
+
return undefined;
|
|
155
|
+
const m = /--spawned-by\s+.*?"pid"\s*:\s*(\d+)/.exec(args);
|
|
156
|
+
if (!m)
|
|
157
|
+
return undefined;
|
|
158
|
+
const pid = parseInt(m[1], 10);
|
|
159
|
+
return Number.isFinite(pid) && pid > 1 ? pid : undefined;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
export const DETACHED_HELPER_RULES = [CLAUDE_BG_DAEMON];
|
|
163
|
+
/**
|
|
164
|
+
* Long-lived agents-cli services that must never be reaped, even when they
|
|
165
|
+
* inherited a pane's environment because the CLI invocation that started them
|
|
166
|
+
* happened to run inside an agent pane.
|
|
167
|
+
*
|
|
168
|
+
* This is a safety invariant, not a fallback: the routines daemon runs this very
|
|
169
|
+
* reaper, so killing it would make the reaper delete its own executor, and the
|
|
170
|
+
* secrets broker holds the keychain session every live agent depends on.
|
|
171
|
+
*/
|
|
172
|
+
export function isProtectedAgentsService(args) {
|
|
173
|
+
return /\b__daemon-run\b/.test(args)
|
|
174
|
+
|| /\bsecrets\s+_agent-run\b/.test(args)
|
|
175
|
+
|| /\bMenubarHelper\b/.test(args)
|
|
176
|
+
|| /\bAgents CLI\.app\b/.test(args);
|
|
177
|
+
}
|
|
178
|
+
/** Is this pid currently running? Signal-0 probe, matching `platform/process.ts`. */
|
|
179
|
+
function livePid(pid) {
|
|
180
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
181
|
+
return false;
|
|
182
|
+
try {
|
|
183
|
+
process.kill(pid, 0);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
// EPERM means the process exists but belongs to another user — alive.
|
|
188
|
+
return err.code === 'EPERM';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Pure. Select the processes whose owning agent is provably gone.
|
|
193
|
+
*
|
|
194
|
+
* `owners` maps a tmux session name to its liveness. An ABSENT name is never
|
|
195
|
+
* proof that the marked process is orphaned: the tmux server can restart or its
|
|
196
|
+
* socket can disappear while the pane's process tree is still alive. That exact
|
|
197
|
+
* state made the daemon classify every live agent as `tmux-session-gone` and
|
|
198
|
+
* SIGTERM it on startup (RUSH-2603). Tier 1 therefore requires a PRESENT owner
|
|
199
|
+
* whose pane process is confirmed dead and has no attached client. A reliable
|
|
200
|
+
* empty map means "nothing proven dead", not "every marked process is dead".
|
|
201
|
+
* `opts.ownersReliable` still disables tier 1 when any query failed to answer.
|
|
202
|
+
*/
|
|
203
|
+
export function selectOrphanProcesses(procs, owners, opts) {
|
|
204
|
+
const isAlive = opts.isAlive ?? livePid;
|
|
205
|
+
const ownersReliable = opts.ownersReliable ?? true;
|
|
206
|
+
const eligible = (p) => p.pid > 1 && !opts.protectedPids.has(p.pid) && !isProtectedAgentsService(p.args);
|
|
207
|
+
/**
|
|
208
|
+
* Every pid STILL reachable, right now, from a live pane leaf through ppid
|
|
209
|
+
* links — not just the leaf itself. A live agent's own child (e.g. a Bash
|
|
210
|
+
* tool spawning `claude --print "…daemon run --spawned-by…"` as a
|
|
211
|
+
* sub-invocation) inherits nothing from `livePanePids` (that set only ever
|
|
212
|
+
* contains the leaf's own pid) unless we walk the tree, and on macOS that
|
|
213
|
+
* child ALSO carries no readable env marker (tier 1 is dead there) — so
|
|
214
|
+
* without this expansion, the "must never become a kill seed, whatever its
|
|
215
|
+
* own argv says" guarantee only held for the leaf itself, not the live
|
|
216
|
+
* agent's actual subprocess tree the same docblock's threat model names
|
|
217
|
+
* (RUSH-2521 review, round 3). A process that has genuinely reparented away
|
|
218
|
+
* (a real detached daemon, ppid now 1 or otherwise no longer chained to the
|
|
219
|
+
* leaf) is NOT in this set and remains reapable — this only protects what
|
|
220
|
+
* is still structurally part of the live agent's own tree right now.
|
|
221
|
+
*/
|
|
222
|
+
const livePaneSubtree = opts.livePanePids && opts.livePanePids.size > 0
|
|
223
|
+
? new Set(descendantsOf(procs, [...opts.livePanePids]))
|
|
224
|
+
: undefined;
|
|
225
|
+
/**
|
|
226
|
+
* A process still owned by a live/attached pane can never seed a kill,
|
|
227
|
+
* whatever its own argv says. Two independent signals, either sufficient:
|
|
228
|
+
* the process is still IN a live pane leaf's process tree right now (tmux's
|
|
229
|
+
* own data, no marker needed), or it carries a marker whose session is
|
|
230
|
+
* live/attached.
|
|
231
|
+
*/
|
|
232
|
+
const ownedByLivePane = (p) => {
|
|
233
|
+
if (livePaneSubtree?.has(p.pid))
|
|
234
|
+
return true;
|
|
235
|
+
if (!p.tmuxSession)
|
|
236
|
+
return false;
|
|
237
|
+
const owner = owners.get(p.tmuxSession);
|
|
238
|
+
return !!owner && (owner.agentAlive || owner.attached);
|
|
239
|
+
};
|
|
240
|
+
const out = [];
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
const push = (p, reason) => {
|
|
243
|
+
if (seen.has(p.pid))
|
|
244
|
+
return;
|
|
245
|
+
seen.add(p.pid);
|
|
246
|
+
out.push({ pid: p.pid, args: p.args, reason, tmuxSession: p.tmuxSession });
|
|
247
|
+
};
|
|
248
|
+
// Tier 1 — inherited pane marker with a dead owner. Skipped whole when the
|
|
249
|
+
// owners read is unreliable (see the docstring above).
|
|
250
|
+
if (ownersReliable) {
|
|
251
|
+
for (const p of procs) {
|
|
252
|
+
if (!p.tmuxSession || !eligible(p))
|
|
253
|
+
continue;
|
|
254
|
+
const owner = owners.get(p.tmuxSession);
|
|
255
|
+
if (!owner)
|
|
256
|
+
continue;
|
|
257
|
+
// An attached client or a live agent pane process is a hard exclusion.
|
|
258
|
+
if (owner.attached || owner.agentAlive)
|
|
259
|
+
continue;
|
|
260
|
+
push(p, 'tmux-agent-exited');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Tier 2 — a detached helper daemon whose declared spawner is dead, plus the
|
|
264
|
+
// workers it owns. Its own descendants are pulled in because they carry no
|
|
265
|
+
// owner declaration of their own. A candidate seed that still carries a
|
|
266
|
+
// LIVE/attached pane marker is excluded even if its argv matches a rule: it
|
|
267
|
+
// is provably not a detached daemon, whatever text its command line quotes.
|
|
268
|
+
const seeds = [];
|
|
269
|
+
for (const p of procs) {
|
|
270
|
+
if (!eligible(p) || ownedByLivePane(p))
|
|
271
|
+
continue;
|
|
272
|
+
for (const rule of DETACHED_HELPER_RULES) {
|
|
273
|
+
const spawner = rule.spawnerPid(p.args);
|
|
274
|
+
if (spawner === undefined || isAlive(spawner))
|
|
275
|
+
continue;
|
|
276
|
+
seeds.push(p);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (seeds.length > 0) {
|
|
281
|
+
const byPid = new Map(procs.map(p => [p.pid, p]));
|
|
282
|
+
for (const seed of descendantsOf(procs, seeds.map(s => s.pid))) {
|
|
283
|
+
const p = byPid.get(seed);
|
|
284
|
+
if (p && eligible(p))
|
|
285
|
+
push(p, 'detached-helper');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Pure. `seeds` plus every process reachable from them through ppid links.
|
|
292
|
+
* Depth is bounded by the table size, so a ppid cycle (which the kernel cannot
|
|
293
|
+
* produce, but a malformed snapshot could) terminates.
|
|
294
|
+
*/
|
|
295
|
+
export function descendantsOf(procs, seeds) {
|
|
296
|
+
const children = new Map();
|
|
297
|
+
for (const p of procs) {
|
|
298
|
+
const list = children.get(p.ppid);
|
|
299
|
+
if (list)
|
|
300
|
+
list.push(p.pid);
|
|
301
|
+
else
|
|
302
|
+
children.set(p.ppid, [p.pid]);
|
|
303
|
+
}
|
|
304
|
+
const out = [];
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
const stack = [...seeds];
|
|
307
|
+
while (stack.length > 0) {
|
|
308
|
+
const pid = stack.pop();
|
|
309
|
+
if (seen.has(pid))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(pid);
|
|
312
|
+
out.push(pid);
|
|
313
|
+
for (const child of children.get(pid) ?? [])
|
|
314
|
+
stack.push(child);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/** Pure. Parse `ps -A -o pid=,ppid=,args=` output. */
|
|
319
|
+
export function parseProcessRows(stdout) {
|
|
320
|
+
const rows = [];
|
|
321
|
+
for (const line of stdout.split('\n')) {
|
|
322
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
323
|
+
if (!m)
|
|
324
|
+
continue;
|
|
325
|
+
const pid = parseInt(m[1], 10);
|
|
326
|
+
const ppid = parseInt(m[2], 10);
|
|
327
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid))
|
|
328
|
+
continue;
|
|
329
|
+
rows.push({ pid, ppid, args: m[3].trim() });
|
|
330
|
+
}
|
|
331
|
+
return rows;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Pure. Extract the pane marker from a blob that contains the process
|
|
335
|
+
* environment — a NUL-separated `/proc/<pid>/environ` on Linux, or the
|
|
336
|
+
* space-separated `KEY=VALUE` tail macOS `ps -E` appends after the command.
|
|
337
|
+
* Session names are `[A-Za-z0-9_-]` ({@link ../tmux/session.ts} `VALID_NAME`),
|
|
338
|
+
* so the value ends at the first character outside that set.
|
|
339
|
+
*/
|
|
340
|
+
export function parseTmuxSessionMarker(blob) {
|
|
341
|
+
const m = new RegExp(`(?:^|[\\s\\0])${TMUX_SESSION_ENV}=([A-Za-z0-9_-]{1,64})`).exec(blob);
|
|
342
|
+
return m ? m[1] : undefined;
|
|
343
|
+
}
|
|
344
|
+
/** Pure. Parse `tmux list-panes -a -F '#{session_name}\t#{pane_pid}\t#{session_attached}'`. */
|
|
345
|
+
export function parsePaneOwners(stdout, isAlive = livePid) {
|
|
346
|
+
const owners = new Map();
|
|
347
|
+
for (const line of stdout.split('\n')) {
|
|
348
|
+
const parts = line.trim().split('\t');
|
|
349
|
+
if (parts.length < 3)
|
|
350
|
+
continue;
|
|
351
|
+
const name = parts[0];
|
|
352
|
+
if (!name)
|
|
353
|
+
continue;
|
|
354
|
+
const panePid = parseInt(parts[1], 10);
|
|
355
|
+
const attached = parts[2].trim() !== '0';
|
|
356
|
+
const prev = owners.get(name);
|
|
357
|
+
const agentAlive = Number.isFinite(panePid) && isAlive(panePid);
|
|
358
|
+
owners.set(name, {
|
|
359
|
+
agentAlive: (prev?.agentAlive ?? false) || agentAlive,
|
|
360
|
+
attached: (prev?.attached ?? false) || attached,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return owners;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Pure. Every `#{pane_pid}` tmux reports in the same `list-panes -a` output
|
|
367
|
+
* {@link parsePaneOwners} reads — a pid tmux itself says is a pane's leaf
|
|
368
|
+
* process, independent of whether that process's OWN environment is readable.
|
|
369
|
+
*
|
|
370
|
+
* This is the marker-independent half of pane ownership: {@link parsePaneOwners}
|
|
371
|
+
* answers "is SESSION X owned" by aggregating over every pane a session has;
|
|
372
|
+
* this answers "is PID N itself a pane leaf tmux currently tracks" for exactly
|
|
373
|
+
* one candidate pid, with no dependency on {@link TMUX_SESSION_ENV} ever having
|
|
374
|
+
* been readable for it.
|
|
375
|
+
*/
|
|
376
|
+
export function parsePanePids(stdout) {
|
|
377
|
+
const pids = new Set();
|
|
378
|
+
for (const line of stdout.split('\n')) {
|
|
379
|
+
const parts = line.trim().split('\t');
|
|
380
|
+
if (parts.length < 2)
|
|
381
|
+
continue;
|
|
382
|
+
const pid = parseInt(parts[1], 10);
|
|
383
|
+
if (Number.isFinite(pid))
|
|
384
|
+
pids.add(pid);
|
|
385
|
+
}
|
|
386
|
+
return pids;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Snapshot the process table with each process's pane marker.
|
|
390
|
+
*
|
|
391
|
+
* Two readers because no single command works on both platforms: Linux exposes
|
|
392
|
+
* `/proc/<pid>/environ` (one cheap read per pid), while macOS has no `/proc` and
|
|
393
|
+
* instead lets `ps -E` print a process's own-uid environment after its command
|
|
394
|
+
* — though on modern macOS `ps -E` no longer includes it at all (see the
|
|
395
|
+
* "Known platform gap" section in this file's docblock); tier 1 safely no-ops
|
|
396
|
+
* there rather than misattributing anything. Windows has no tmux integration
|
|
397
|
+
* at all, so it returns an empty table rather than pretending to reap.
|
|
398
|
+
*
|
|
399
|
+
* `opts.pids` restricts the `ps` snapshot to an explicit pid allowlist instead
|
|
400
|
+
* of the whole machine (`-A`) — a test-only escape hatch so a reap exercised
|
|
401
|
+
* against real spawned processes can never select or signal a real, unrelated
|
|
402
|
+
* process on a shared box (RUSH-2521 review: the integration tests previously
|
|
403
|
+
* called the machine-wide reap directly, which is safe only by construction of
|
|
404
|
+
* the fixture, not by anything the reaper itself guarantees).
|
|
405
|
+
*/
|
|
406
|
+
export async function readAgentProcesses(opts = {}) {
|
|
407
|
+
if (process.platform === 'win32')
|
|
408
|
+
return [];
|
|
409
|
+
const scope = opts.pids && opts.pids.length > 0 ? ['-p', opts.pids.join(',')] : ['-A'];
|
|
410
|
+
const hasProc = fs.existsSync('/proc/self/environ');
|
|
411
|
+
const args = hasProc ? [...scope, '-o', 'pid=,ppid=,args='] : [...scope, '-E', '-o', 'pid=,ppid=,args='];
|
|
412
|
+
let stdout;
|
|
413
|
+
try {
|
|
414
|
+
({ stdout } = await execFileAsync('ps', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: PS_TIMEOUT_MS }));
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const rows = parseProcessRows(stdout);
|
|
420
|
+
if (!hasProc) {
|
|
421
|
+
// `ps -E` already appended the environment to the args column.
|
|
422
|
+
for (const row of rows)
|
|
423
|
+
row.tmuxSession = parseTmuxSessionMarker(row.args);
|
|
424
|
+
return rows;
|
|
425
|
+
}
|
|
426
|
+
for (const row of rows) {
|
|
427
|
+
let blob;
|
|
428
|
+
try {
|
|
429
|
+
blob = fs.readFileSync(`/proc/${row.pid}/environ`, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
continue; // exited between ps and the read, or not ours to inspect
|
|
433
|
+
}
|
|
434
|
+
row.tmuxSession = parseTmuxSessionMarker(blob);
|
|
435
|
+
}
|
|
436
|
+
return rows;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Read one tmux socket's session-ownership state.
|
|
440
|
+
*
|
|
441
|
+
* Distinguishes "tmux answered: nothing here" from "tmux did not answer" —
|
|
442
|
+
* the two were previously conflated (`.catch(() => null)` then `!res` folded
|
|
443
|
+
* into the same empty-map return as a clean nonzero exit), which meant a
|
|
444
|
+
* missing/wrong-version tmux, a spawn failure, or a wedged server were
|
|
445
|
+
* indistinguishable from a genuinely empty box. A completed process — even a
|
|
446
|
+
* nonzero exit — is a real answer; a rejected promise is not an answer at all
|
|
447
|
+
* (RUSH-2521 review).
|
|
448
|
+
*/
|
|
449
|
+
export async function readPaneOwners(socket) {
|
|
450
|
+
// No socket file at all means no server was ever started here — for the
|
|
451
|
+
// shared agents socket this is reliable (tmux unlinks its own socket on
|
|
452
|
+
// exit), so this is a confident, reliable EMPTY read, not an unknown one.
|
|
453
|
+
if (!fs.existsSync(socket))
|
|
454
|
+
return { ok: true, owners: new Map(), panePids: new Set() };
|
|
455
|
+
const { runTmux } = await import('./binary.js');
|
|
456
|
+
try {
|
|
457
|
+
const res = await runTmux({
|
|
458
|
+
socket,
|
|
459
|
+
args: ['list-panes', '-a', '-F', '#{session_name}\t#{pane_pid}\t#{session_attached}'],
|
|
460
|
+
throwOnError: false,
|
|
461
|
+
timeoutMs: TMUX_QUERY_TIMEOUT_MS,
|
|
462
|
+
});
|
|
463
|
+
// A completed run answered, whatever its exit code — tmux itself is
|
|
464
|
+
// telling us there's no server/no sessions on a nonzero exit here.
|
|
465
|
+
if (res.code !== 0)
|
|
466
|
+
return { ok: true, owners: new Map(), panePids: new Set() };
|
|
467
|
+
return { ok: true, owners: parsePaneOwners(res.stdout), panePids: parsePanePids(res.stdout) };
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Threw: tmux missing/unsupported version (assertTmuxAvailable), a spawn
|
|
471
|
+
// failure, or the query timed out. We genuinely do not know.
|
|
472
|
+
return { ok: false, owners: new Map(), panePids: new Set() };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Ownership across every socket that could own a marked process, merged so a
|
|
477
|
+
* session that is live on ANY of them protects its processes. `reliable` is
|
|
478
|
+
* `false` the moment ANY queried socket's read failed — the merged map could
|
|
479
|
+
* then be missing entries for sessions that live there, so a caller MUST NOT
|
|
480
|
+
* treat an absent key in it as proof of anything.
|
|
481
|
+
*
|
|
482
|
+
* The union is load bearing, not defensive: the process table is machine-wide,
|
|
483
|
+
* so a reap driven from a non-default socket (`agents sessions reap --socket`,
|
|
484
|
+
* or a test's temp socket) would otherwise see every real agent's marker with no
|
|
485
|
+
* matching session and classify a whole box's live helpers as orphans.
|
|
486
|
+
*/
|
|
487
|
+
async function readAllPaneOwners(socket) {
|
|
488
|
+
const { getDefaultSocketPath } = await import('./paths.js');
|
|
489
|
+
const merged = new Map();
|
|
490
|
+
const panePids = new Set();
|
|
491
|
+
let reliable = true;
|
|
492
|
+
for (const sock of new Set([socket, getDefaultSocketPath()].filter(Boolean))) {
|
|
493
|
+
const read = await readPaneOwners(sock);
|
|
494
|
+
reliable = reliable && read.ok;
|
|
495
|
+
for (const pid of read.panePids)
|
|
496
|
+
panePids.add(pid);
|
|
497
|
+
for (const [name, owner] of read.owners) {
|
|
498
|
+
const prev = merged.get(name);
|
|
499
|
+
merged.set(name, {
|
|
500
|
+
agentAlive: (prev?.agentAlive ?? false) || owner.agentAlive,
|
|
501
|
+
attached: (prev?.attached ?? false) || owner.attached,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { owners: merged, panePids, reliable };
|
|
506
|
+
}
|
|
507
|
+
/** Pids the reaper must never signal: itself and its whole ancestor chain. */
|
|
508
|
+
function selfProtectedPids(procs) {
|
|
509
|
+
const byPid = new Map(procs.map(p => [p.pid, p]));
|
|
510
|
+
const out = new Set([process.pid]);
|
|
511
|
+
let cursor = process.ppid;
|
|
512
|
+
// Bounded by the table size — an ancestor walk cannot exceed it.
|
|
513
|
+
for (let i = 0; i <= procs.length && cursor > 1; i += 1) {
|
|
514
|
+
out.add(cursor);
|
|
515
|
+
cursor = byPid.get(cursor)?.ppid ?? 0;
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
/** SIGTERM every candidate, then SIGKILL whatever ignored it. */
|
|
520
|
+
async function terminate(candidates, graceMs) {
|
|
521
|
+
let signalled = 0;
|
|
522
|
+
for (const c of candidates) {
|
|
523
|
+
try {
|
|
524
|
+
process.kill(c.pid, 'SIGTERM');
|
|
525
|
+
signalled += 1;
|
|
526
|
+
}
|
|
527
|
+
catch { /* already gone */ }
|
|
528
|
+
}
|
|
529
|
+
if (signalled === 0)
|
|
530
|
+
return 0;
|
|
531
|
+
await new Promise(resolve => setTimeout(resolve, graceMs));
|
|
532
|
+
for (const c of candidates) {
|
|
533
|
+
if (!livePid(c.pid))
|
|
534
|
+
continue;
|
|
535
|
+
try {
|
|
536
|
+
process.kill(c.pid, 'SIGKILL');
|
|
537
|
+
}
|
|
538
|
+
catch { /* already gone */ }
|
|
539
|
+
}
|
|
540
|
+
return signalled;
|
|
541
|
+
}
|
|
542
|
+
function describe(c) {
|
|
543
|
+
const where = c.tmuxSession ? ` [${c.tmuxSession}]` : '';
|
|
544
|
+
return `pid ${c.pid}${where} (${c.reason}): ${c.args.slice(0, 120)}`;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Reap every helper process whose owning agent has exited.
|
|
548
|
+
*
|
|
549
|
+
* Called from the daemon's periodic tick and from `agents sessions reap`. Panes
|
|
550
|
+
* are read before processes to take one ownership snapshot. A session absent
|
|
551
|
+
* from that snapshot is unknown and is never sufficient proof for tier 1.
|
|
552
|
+
*
|
|
553
|
+
* `opts.pids` is the test-only process-table scope described on
|
|
554
|
+
* {@link readAgentProcesses} — never set by production callers.
|
|
555
|
+
*/
|
|
556
|
+
export async function reapOrphanAgentProcesses(opts = { socket: '' }) {
|
|
557
|
+
const result = { killed: 0, details: [], candidates: [], warnings: [] };
|
|
558
|
+
if (process.platform === 'win32')
|
|
559
|
+
return result;
|
|
560
|
+
const { owners, panePids, reliable } = await readAllPaneOwners(opts.socket);
|
|
561
|
+
if (!reliable) {
|
|
562
|
+
result.warnings.push('tier 1 (pane-marker) sweep skipped this tick: a tmux session query failed to answer');
|
|
563
|
+
}
|
|
564
|
+
const procs = await readAgentProcesses({ pids: opts.pids });
|
|
565
|
+
if (procs.length === 0)
|
|
566
|
+
return result;
|
|
567
|
+
const candidates = selectOrphanProcesses(procs, owners, {
|
|
568
|
+
protectedPids: selfProtectedPids(procs),
|
|
569
|
+
ownersReliable: reliable,
|
|
570
|
+
livePanePids: panePids,
|
|
571
|
+
});
|
|
572
|
+
result.candidates = candidates;
|
|
573
|
+
if (candidates.length === 0)
|
|
574
|
+
return result;
|
|
575
|
+
result.details = candidates.map(describe);
|
|
576
|
+
if (opts.dryRun)
|
|
577
|
+
return result;
|
|
578
|
+
result.killed = await terminate(candidates, opts.graceMs ?? REAP_GRACE_MS);
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Reap the helper processes belonging to ONE tmux session, on the teardown path.
|
|
583
|
+
*
|
|
584
|
+
* `killSession` calls this so an agent's helpers die with the session that owned
|
|
585
|
+
* them instead of waiting for the next daemon tick. The tmux session is being
|
|
586
|
+
* destroyed by the caller, so ownership needs no liveness check — anything still
|
|
587
|
+
* carrying this session's marker is leftover by definition, PROVIDED the name
|
|
588
|
+
* is unique to the socket in play.
|
|
589
|
+
*
|
|
590
|
+
* `socket` narrows that: a session name is only unique WITHIN one tmux server,
|
|
591
|
+
* so the exact same marker value could legitimately belong to a live, unrelated
|
|
592
|
+
* session on a DIFFERENT socket (e.g. the shared agents socket vs. a test's own
|
|
593
|
+
* temp socket). When `socket` names a non-default socket, this checks the
|
|
594
|
+
* default agents socket for a live/attached session of the same name and backs
|
|
595
|
+
* off entirely if one exists — the caller is tearing down one session, not a
|
|
596
|
+
* same-named one it does not own.
|
|
597
|
+
*
|
|
598
|
+
* `opts.pids` is the test-only process-table scope described on
|
|
599
|
+
* {@link readAgentProcesses} — never set by production callers.
|
|
600
|
+
*/
|
|
601
|
+
export async function reapProcessesForTmuxSession(name, socket, opts = {}) {
|
|
602
|
+
const result = { killed: 0, details: [], candidates: [], warnings: [] };
|
|
603
|
+
if (process.platform === 'win32')
|
|
604
|
+
return result;
|
|
605
|
+
const { getDefaultSocketPath } = await import('./paths.js');
|
|
606
|
+
const defaultSocket = getDefaultSocketPath();
|
|
607
|
+
if (socket && socket !== defaultSocket) {
|
|
608
|
+
const elsewhere = await readPaneOwners(defaultSocket);
|
|
609
|
+
const owner = elsewhere.owners.get(name);
|
|
610
|
+
if (owner && (owner.agentAlive || owner.attached)) {
|
|
611
|
+
result.warnings.push(`skipped: "${name}" is a live session on the agents socket, not the one being torn down`);
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const procs = await readAgentProcesses({ pids: opts.pids });
|
|
616
|
+
if (procs.length === 0)
|
|
617
|
+
return result;
|
|
618
|
+
const protectedPids = selfProtectedPids(procs);
|
|
619
|
+
const byPid = new Map(procs.map(p => [p.pid, p]));
|
|
620
|
+
const owned = procs.filter(p => p.tmuxSession === name);
|
|
621
|
+
// Descendants too: a helper that scrubbed its own environment (so it carries
|
|
622
|
+
// no marker) is still attributable through the marked process that spawned it.
|
|
623
|
+
const candidates = [];
|
|
624
|
+
for (const pid of descendantsOf(procs, owned.map(p => p.pid))) {
|
|
625
|
+
const p = byPid.get(pid);
|
|
626
|
+
if (!p || p.pid <= 1 || protectedPids.has(p.pid) || isProtectedAgentsService(p.args))
|
|
627
|
+
continue;
|
|
628
|
+
candidates.push({ pid: p.pid, args: p.args, reason: 'tmux-agent-exited', tmuxSession: name });
|
|
629
|
+
}
|
|
630
|
+
result.candidates = candidates;
|
|
631
|
+
if (candidates.length === 0)
|
|
632
|
+
return result;
|
|
633
|
+
result.details = candidates.map(describe);
|
|
634
|
+
if (opts.dryRun)
|
|
635
|
+
return result;
|
|
636
|
+
result.killed = await terminate(candidates, opts.graceMs ?? REAP_GRACE_MS);
|
|
637
|
+
return result;
|
|
638
|
+
}
|