@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.
Files changed (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pick a worker device for offloaded machine work — the suite, a build, any job
3
+ * that pins a box for minutes and must never land on the machine someone is
4
+ * sitting at.
5
+ *
6
+ * This is deliberately NOT `resolveDeviceAuto`. That resolver answers "which box
7
+ * should run this AGENT", so it gates on harness installation and a launch-ready
8
+ * account (`smart-launch.ts:200-210`). A vitest run needs neither: it needs a
9
+ * reachable POSIX box with headroom. Gating a test offload on a signed-in Claude
10
+ * account would exclude a perfectly good worker for a reason that has nothing to
11
+ * do with running tests.
12
+ *
13
+ * What it DOES share, on purpose, is everything that decides *where agents may
14
+ * go*: the same {@link listOnlineDeviceNames} pool (so `role=worker` /
15
+ * `role=personal` marks move this surface too), the same
16
+ * {@link probePoolSignals} reachability+load probe (so one cached fleet probe
17
+ * serves both), and the same {@link pickBestDevice} least-loaded ranking. Before
18
+ * this module, `scripts/test.sh` hand-rolled its own resolver in inline Python
19
+ * and therefore could not offer `--device auto` at all.
20
+ */
21
+ import { normalizeHost } from '../machine-id.js';
22
+ import { localMachineId } from '../session/origin-machine.js';
23
+ import { loadDevicesSync } from './registry.js';
24
+ import { isAutoPoolMember } from './pool.js';
25
+ import { formatEmptyAutoPoolError, listOnlineDeviceNames, } from '../smart-launch.js';
26
+ import { pickBestDevice } from '../teams/scheduler.js';
27
+ import { probePoolSignals } from '../teams/placement-probe.js';
28
+ /**
29
+ * Platforms a bun/vitest offload can actually run on.
30
+ *
31
+ * Windows is excluded by policy, not by accident: `AGENTS.md` states Windows is
32
+ * not a required PR or ordinary-release platform and `tests.yml` runs it only as
33
+ * a `continue-on-error` post-merge smoke. Silently shipping the tree to
34
+ * `win-mini` would produce a confusing failure for a platform whose result
35
+ * nobody gates on.
36
+ */
37
+ export const POSIX_PLATFORMS = ['linux', 'macos'];
38
+ /**
39
+ * Resolve the least-loaded eligible worker.
40
+ *
41
+ * Throws — never returns a degraded answer. An offload helper that quietly
42
+ * decided "no worker, run here" would recreate exactly the failure the offload
43
+ * exists to prevent: the operator believes the work went to the fleet while
44
+ * their laptop is pinned. Callers surface the thrown message verbatim.
45
+ */
46
+ export async function resolveWorkerDevice(opts = {}) {
47
+ const local = normalizeHost(opts.localMachine ?? localMachineId());
48
+ const pool = [...new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost))];
49
+ // The local box joins the pool only when a role does not exclude it — the same
50
+ // rule resolveDeviceAuto applies. A box marked `personal` is marked precisely
51
+ // so long jobs stay off it, and that mark must hold for the suite too.
52
+ if (!pool.includes(local) && isAutoPoolMember(local))
53
+ pool.push(local);
54
+ if (pool.length === 0)
55
+ throw new Error(formatEmptyAutoPoolError());
56
+ const excluded = [];
57
+ const wanted = new Set((opts.platforms ?? POSIX_PLATFORMS).map((p) => p.toLowerCase()));
58
+ const reg = loadDevicesSync();
59
+ const platformOf = (name) => {
60
+ const d = reg[name] ?? reg[normalizeHost(name)]
61
+ ?? Object.values(reg).find((p) => p && normalizeHost(p.name) === normalizeHost(name));
62
+ return d?.platform ? String(d.platform).toLowerCase() : undefined;
63
+ };
64
+ // Platform first: it is a static registry fact, so filtering here keeps the
65
+ // live probe off boxes that could never be picked anyway.
66
+ const onPlatform = pool.filter((device) => {
67
+ const platform = platformOf(device);
68
+ // Unknown platform stays a candidate — a registry-only box with no recorded
69
+ // platform is a gap in the registry, not positive evidence it is Windows.
70
+ if (platform && !wanted.has(platform)) {
71
+ excluded.push({ device, reason: 'wrong-platform' });
72
+ return false;
73
+ }
74
+ return true;
75
+ });
76
+ if (onPlatform.length === 0)
77
+ throw new Error(formatNoWorkerError(excluded, wanted));
78
+ const signals = await (opts.probe ?? ((p) => probePoolSignals(p)))(onPlatform);
79
+ const eligible = onPlatform.filter((device) => {
80
+ const signal = signals.get(device);
81
+ if (signal?.reachable !== true) {
82
+ excluded.push({ device, reason: 'unreachable' });
83
+ return false;
84
+ }
85
+ if (signal.headroom === 'loaded') {
86
+ excluded.push({ device, reason: 'overloaded' });
87
+ return false;
88
+ }
89
+ return true;
90
+ });
91
+ if (eligible.length === 0)
92
+ throw new Error(formatNoWorkerError(excluded, wanted));
93
+ const device = pickBestDevice(eligible, [], { signals });
94
+ return {
95
+ device,
96
+ isLocal: normalizeHost(device) === local,
97
+ candidates: onPlatform.map((key) => ({
98
+ device: key,
99
+ loadPercent: signals.get(key)?.loadPercent,
100
+ headroom: signals.get(key)?.headroom,
101
+ })),
102
+ excluded,
103
+ };
104
+ }
105
+ /** The one no-worker message, naming every candidate and why it was dropped. */
106
+ export function formatNoWorkerError(excluded, platforms) {
107
+ const detail = excluded.length
108
+ ? excluded.map((e) => `${e.device} (${e.reason})`).join(', ')
109
+ : 'none';
110
+ return (`agents: no worker device is available for offloaded work `
111
+ + `[platforms: ${[...platforms].sort().join(', ')}] — excluded: ${detail}. `
112
+ + 'Mark a worker with `agents devices role <name> worker`, or name one explicitly.');
113
+ }
@@ -94,6 +94,8 @@ export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
94
94
  export interface ExecOptions {
95
95
  agent: AgentId;
96
96
  version?: string;
97
+ /** Version home whose native auth/config is overlaid onto this run's binary. */
98
+ configVersion?: string;
97
99
  /** Omit to launch the CLI interactively -- no prompt, no --print, stdio fully inherited. */
98
100
  prompt?: string;
99
101
  /** Force interactive mode even when a prompt is provided. Wins over `headless`. */
@@ -398,30 +400,67 @@ export interface TmuxWrapContext {
398
400
  raw: boolean;
399
401
  /** The AGENTS_NO_TMUX=1 escape hatch. */
400
402
  noTmuxEnv: boolean;
401
- /** This device's `tmux.enabled` config — false turns the wrap off for every launch on this box. */
403
+ /** This device's `tmux.enabled` config — true opts every eligible launch on this box into the wrap. */
402
404
  configEnabled: boolean;
405
+ /**
406
+ * True when this run was dispatched onto this box over SSH by `--device`
407
+ * (the launcher exports {@link REMOTE_INTERACTIVE_ENV}).
408
+ *
409
+ * A remote interactive agent is a child of the sshd session and holds its
410
+ * controlling TTY, so without the wrap a dropped link SIGHUPs it and the work
411
+ * in flight is gone. Durability is therefore NOT a preference the way
412
+ * `configEnabled` is — `tmux.enabled` is about whether this box's operator
413
+ * likes tmux's mouse/clipboard/scrollback at their own keyboard, which says
414
+ * nothing about whether a run arriving over the network must outlive it.
415
+ * Conflating the two is what left every `--device` agent unsurvivable
416
+ * (RUSH-3125), while lib/hosts/reconnect.ts reconnected on the premise that
417
+ * they were detached.
418
+ */
419
+ remoteDispatch: boolean;
403
420
  /** Whether a tmux binary is on PATH. */
404
421
  tmuxAvailable: boolean;
405
422
  }
423
+ /**
424
+ * What to do with an interactive spawn. Three outcomes, not two: a run that
425
+ * MUST be durable and cannot be is neither "wrap" nor "spawn bare" — it is a
426
+ * launch that should not happen, because a bare remote spawn looks fine right
427
+ * up until the link blinks and the agent dies with it.
428
+ */
429
+ export type TmuxWrapDecision =
430
+ /** Run the agent in a detached tmux session and attach this TTY. */
431
+ {
432
+ kind: 'wrap';
433
+ }
434
+ /** Spawn directly — nothing about this run needs a pane. */
435
+ | {
436
+ kind: 'bare';
437
+ }
438
+ /** Remote-dispatched and tmux is missing on this box: refuse, don't pretend. */
439
+ | {
440
+ kind: 'undurable';
441
+ };
406
442
  /**
407
443
  * Decide whether to run an interactive agent INSIDE a detached tmux session on
408
444
  * the shared socket (then attach the current TTY) instead of a bare spawn.
409
445
  *
410
- * tmux-wrapping gives every interactive agent an exact, unique `%pane` handle so
411
- * `agents sessions --active` can tell co-located agents apart, and lets `agents
412
- * focus` re-attach a live session without forking it. Pure so the gate is unit-
413
- * tested independently of the (side-effecting) spawn.
446
+ * Wrapping serves two independent purposes, and they are gated differently:
447
+ *
448
+ * - **Addressability** (`configEnabled`): a unique `%pane` handle so
449
+ * `agents sessions --active` can tell co-located agents apart and `agents
450
+ * focus` re-attaches without forking. That is a local preference — the
451
+ * operator turns it on once tmux's mouse/clipboard/scrollback suits them.
452
+ * - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
453
+ * the SSH client, because a bare remote spawn dies of SIGHUP the moment the
454
+ * link blinks. That is not a preference, so it does not consult
455
+ * `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
414
456
  *
415
- * All seven guards must pass:
416
- * - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
417
- * - not Windows — no tmux path on win32.
418
- * - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
419
- * - not --raw — explicit opt-out.
420
- * - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
421
- * - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
422
- * - tmux installed — otherwise there is nothing to wrap with.
457
+ * The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
458
+ * explicit "I want the bare process" requests, and honouring them over the
459
+ * durability rule keeps one escape hatch that always works.
460
+ *
461
+ * Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
423
462
  */
424
- export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
463
+ export declare function resolveTmuxWrap(ctx: TmuxWrapContext): TmuxWrapDecision;
425
464
  /**
426
465
  * Build the shell command that runs an agent inside a tmux pane with the exact
427
466
  * env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
@@ -440,6 +479,23 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
440
479
  * into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
441
480
  * real values; the stored/informational copy uses the redacted form (RUSH-1758).
442
481
  */
482
+ /**
483
+ * True when `options.sessionId` is an id the HARNESS actually received, and so a
484
+ * real, resumable handle — rather than one the launcher generated for its own
485
+ * bookkeeping and the agent never saw.
486
+ *
487
+ * Mirrors exactly the two branches in buildExecCommand that put the id on the
488
+ * command line: a native resume (any harness with a `resume` spec) and claude's
489
+ * create-with-`--session-id`. Everything else — `agents run codex --session-id X`
490
+ * without `--resume`, or a fresh non-claude launch whose real id only shows up
491
+ * later via the SessionStart hook — leaves the agent with an id of its own
492
+ * choosing, so `options.sessionId` must not be published as if it were that id.
493
+ *
494
+ * Keep in lockstep with buildExecCommand's resume/create branches: a harness
495
+ * that gains a forced-session-id flag must be reflected here in the SAME change,
496
+ * or this reports a genuine id as fabricated.
497
+ */
498
+ export declare function isHarnessKnownSessionId(agent: AgentId, sessionId: string | undefined, resume: boolean | undefined): boolean;
443
499
  export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
444
500
  redactEnvValues?: boolean;
445
501
  envFile?: string;
package/dist/lib/exec.js CHANGED
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
8
8
  import { randomUUID } from 'crypto';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { ALL_MODES } from './types.js';
11
+ import { ALL_MODES, REMOTE_INTERACTIVE_ENV } from './types.js';
12
12
  import { AGENTS, findInPath } from './agents.js';
13
13
  import { parseTimeout } from './scheduling/routines.js';
14
14
  import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
@@ -28,7 +28,7 @@ import { recordRunName } from './session/run-names.js';
28
28
  import { mailboxDir, isValidMailboxId } from './mailbox.js';
29
29
  import { composeWin32CommandLine } from './platform/index.js';
30
30
  import { isTmuxInstalled } from './tmux/binary.js';
31
- import { isTmuxEnabled } from './device-config.js';
31
+ import { isTmuxEnabled, selfConfiguredDeviceRole } from './device-config.js';
32
32
  import { machineId } from './machine-id.js';
33
33
  import { shellQuote } from './ssh-exec.js';
34
34
  import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
@@ -293,17 +293,28 @@ export function buildExecEnv(options) {
293
293
  // Resolve version/versionHome here (this module already imports
294
294
  // installations/versions); the adapters must not import it themselves or they
295
295
  // close a versions -> shims -> harness -> adapter import cycle.
296
- const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.version);
296
+ const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
297
297
  configAdapter.applyExecConfigEnv(result, {
298
298
  agent: options.agent,
299
299
  version,
300
300
  versionHome,
301
301
  interactive: resolveInteractive(options),
302
+ deviceRole: selfConfiguredDeviceRole(),
302
303
  resolveClaudeSetupToken,
303
304
  });
304
305
  }
305
306
  else {
306
307
  stripForeignConfigDir(result);
308
+ // Grok's adapter owns the same GROK_HOME export in generated shims, but a
309
+ // labeled account deliberately runs one version's binary with another
310
+ // version's config. Apply that overlay here because the shim's binary-slot
311
+ // export cannot express a distinct account slot.
312
+ if (options.agent === 'grok' && options.configVersion) {
313
+ const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion);
314
+ if (!versionHome)
315
+ throw new Error(`Cannot resolve Grok account config home for ${options.configVersion}.`);
316
+ result.GROK_HOME = path.join(versionHome, '.grok');
317
+ }
307
318
  }
308
319
  // Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
309
320
  // knows which box to drain and inject mid-run. Keyed by the session id — the
@@ -322,6 +333,7 @@ export function buildExecEnv(options) {
322
333
  // every event the child emits. `options.sessionId` is the CHILD's id, so read the
323
334
  // spawner from the live env; guard a same-session resume from naming itself parent.
324
335
  // Local-spawn scope here; forwarding it across the `--device` SSH hop is Phase 4.
336
+ delete result.AGENTS_PARENT_SESSION_ID;
325
337
  const spawnerSessionId = process.env.AGENTS_SESSION_ID || process.env.AGENT_SESSION_ID;
326
338
  if (spawnerSessionId && spawnerSessionId !== options.sessionId) {
327
339
  result.AGENTS_PARENT_SESSION_ID = spawnerSessionId;
@@ -388,6 +400,7 @@ export const AGENT_COMMANDS = {
388
400
  // independent; legacy --sandbox flags cannot express that combination.
389
401
  plan: [],
390
402
  edit: [],
403
+ auto: [],
391
404
  // skip = codex --yolo: drops the sandbox entirely and approves anything.
392
405
  skip: ['--dangerously-bypass-approvals-and-sandbox'],
393
406
  },
@@ -1163,36 +1176,47 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
1163
1176
  * Decide whether to run an interactive agent INSIDE a detached tmux session on
1164
1177
  * the shared socket (then attach the current TTY) instead of a bare spawn.
1165
1178
  *
1166
- * tmux-wrapping gives every interactive agent an exact, unique `%pane` handle so
1167
- * `agents sessions --active` can tell co-located agents apart, and lets `agents
1168
- * focus` re-attach a live session without forking it. Pure so the gate is unit-
1169
- * tested independently of the (side-effecting) spawn.
1179
+ * Wrapping serves two independent purposes, and they are gated differently:
1180
+ *
1181
+ * - **Addressability** (`configEnabled`): a unique `%pane` handle so
1182
+ * `agents sessions --active` can tell co-located agents apart and `agents
1183
+ * focus` re-attaches without forking. That is a local preference — the
1184
+ * operator turns it on once tmux's mouse/clipboard/scrollback suits them.
1185
+ * - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
1186
+ * the SSH client, because a bare remote spawn dies of SIGHUP the moment the
1187
+ * link blinks. That is not a preference, so it does not consult
1188
+ * `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
1189
+ *
1190
+ * The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
1191
+ * explicit "I want the bare process" requests, and honouring them over the
1192
+ * durability rule keeps one escape hatch that always works.
1170
1193
  *
1171
- * All seven guards must pass:
1172
- * - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
1173
- * - not Windows — no tmux path on win32.
1174
- * - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
1175
- * - not --raw — explicit opt-out.
1176
- * - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
1177
- * - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
1178
- * - tmux installed — otherwise there is nothing to wrap with.
1194
+ * Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
1179
1195
  */
1180
- export function shouldWrapInTmux(ctx) {
1196
+ export function resolveTmuxWrap(ctx) {
1197
+ // A headless `-p` run has no TTY to attach, Windows has no tmux path, and
1198
+ // nesting tmux-in-tmux is pointless — none of these can wrap, and none of them
1199
+ // is a remote interactive agent whose life depends on it.
1181
1200
  if (!ctx.interactive)
1182
- return false;
1201
+ return { kind: 'bare' };
1183
1202
  if (ctx.platform === 'win32')
1184
- return false;
1203
+ return { kind: 'bare' };
1185
1204
  if (ctx.inTmux)
1186
- return false;
1205
+ return { kind: 'bare' };
1206
+ // Explicit opt-outs win over the durability rule: `--raw` exists precisely to
1207
+ // get the unwrapped process, and a flag that silently stopped working on a
1208
+ // remote box would be worse than an undurable run the user asked for.
1187
1209
  if (ctx.raw)
1188
- return false;
1210
+ return { kind: 'bare' };
1189
1211
  if (ctx.noTmuxEnv)
1190
- return false;
1191
- if (!ctx.configEnabled)
1192
- return false;
1212
+ return { kind: 'bare' };
1213
+ if (!ctx.configEnabled && !ctx.remoteDispatch)
1214
+ return { kind: 'bare' };
1215
+ // Fail loud rather than launch a remote agent that a blink would kill: the
1216
+ // caller refuses the run instead of starting work that cannot be recovered.
1193
1217
  if (!ctx.tmuxAvailable)
1194
- return false;
1195
- return true;
1218
+ return ctx.remoteDispatch ? { kind: 'undurable' } : { kind: 'bare' };
1219
+ return { kind: 'wrap' };
1196
1220
  }
1197
1221
  /**
1198
1222
  * Build the shell command that runs an agent inside a tmux pane with the exact
@@ -1212,6 +1236,29 @@ export function shouldWrapInTmux(ctx) {
1212
1236
  * into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
1213
1237
  * real values; the stored/informational copy uses the redacted form (RUSH-1758).
1214
1238
  */
1239
+ /**
1240
+ * True when `options.sessionId` is an id the HARNESS actually received, and so a
1241
+ * real, resumable handle — rather than one the launcher generated for its own
1242
+ * bookkeeping and the agent never saw.
1243
+ *
1244
+ * Mirrors exactly the two branches in buildExecCommand that put the id on the
1245
+ * command line: a native resume (any harness with a `resume` spec) and claude's
1246
+ * create-with-`--session-id`. Everything else — `agents run codex --session-id X`
1247
+ * without `--resume`, or a fresh non-claude launch whose real id only shows up
1248
+ * later via the SessionStart hook — leaves the agent with an id of its own
1249
+ * choosing, so `options.sessionId` must not be published as if it were that id.
1250
+ *
1251
+ * Keep in lockstep with buildExecCommand's resume/create branches: a harness
1252
+ * that gains a forced-session-id flag must be reflected here in the SAME change,
1253
+ * or this reports a genuine id as fabricated.
1254
+ */
1255
+ export function isHarnessKnownSessionId(agent, sessionId, resume) {
1256
+ if (!sessionId)
1257
+ return false;
1258
+ if (resume && AGENT_COMMANDS[agent]?.resume)
1259
+ return true;
1260
+ return agent === 'claude';
1261
+ }
1215
1262
  export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
1216
1263
  const agentCmd = [executable, ...args].map(shellQuote).join(' ');
1217
1264
  // envFile: source the values instead of inlining them, so no VALUE ever lands
@@ -1282,11 +1329,14 @@ export function formatPaneTail(raw, maxLines = 30) {
1282
1329
  * Lifecycle:
1283
1330
  * 1. createSession() launches `sh -c 'exec env … agent'` detached, remain-on-exit
1284
1331
  * on (global), and returns the pane id.
1285
- * 2. A per-session `pane-died` hook detaches the attach client the instant the
1286
- * AGENT pane exits, so attach returns instead of parking on a dead pane. The
1287
- * hook is guarded on `#{hook_pane}` so it fires ONLY for the agent pane —
1288
- * user-created splits (Ctrl-b " / %) that the user exits are closed in place
1289
- * (`kill-pane`) instead of tearing down the whole client, so exiting one
1332
+ * 2. A per-session `pane-died` hook tears the session down the instant the AGENT
1333
+ * pane exits: with a client attached it `detach-client`s so attach returns
1334
+ * instead of parking on a dead pane (then resolveAfterAttach reads the status
1335
+ * and kills); with no client attached it `kill-session`s outright, so a
1336
+ * wrapped agent that exits unattended can't leave a dead husk lingering on the
1337
+ * socket. The hook is guarded on `#{hook_pane}` so it fires ONLY for the agent
1338
+ * pane — user-created splits (Ctrl-b " / %) that the user exits are closed in
1339
+ * place (`kill-pane`) instead of tearing down the whole client, so exiting one
1290
1340
  * split leaves the agent running full-window rather than kicking you out.
1291
1341
  * 3. We record the agent pane's pid → session mapping (WITH the tmux pane) so the
1292
1342
  * headless active-scan attributes it, then attach the TTY (blocking).
@@ -1303,7 +1353,7 @@ async function runInTmux(options, executable, args) {
1303
1353
  const idSeed = (options.sessionId ?? randomUUID()).slice(0, 8);
1304
1354
  const name = slugifyName(`ag-${options.agent}-${idSeed}`);
1305
1355
  const RED = '\x1b[31m', GRAY = '\x1b[90m', OFF = '\x1b[0m';
1306
- const NO_TMUX_TIP = `${GRAY} Tip: re-run with --no-tmux to launch the agent directly and see its full output.\n If tmux is broken on this machine, turn the wrap off for good: agents config set devices.${machineId()}.tmux off${OFF}\n\n`;
1356
+ const NO_TMUX_TIP = `${GRAY} This run used the opt-in tmux wrap. Re-run with --no-tmux for a direct launch, or turn the wrap off: agents config set devices.${machineId()}.tmux off${OFF}\n\n`;
1307
1357
  // Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
1308
1358
  // detaches the client the instant the agent exits, so a fast failure (a
1309
1359
  // gutted install that dies with ENOENT, a bad flag, a crash on startup) would
@@ -1414,8 +1464,13 @@ async function runInTmux(options, executable, args) {
1414
1464
  const cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
1415
1465
  const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
1416
1466
  const labels = { agent: options.agent };
1417
- if (options.sessionId)
1467
+ // Only publish an id the harness actually received. createSession turns this
1468
+ // label into the `@ag_session_id` tmux option that status bars read, and a
1469
+ // launcher-internal id displayed there looks exactly like a resumable one
1470
+ // while `ag focus <id>` rejects it.
1471
+ if (isHarnessKnownSessionId(options.agent, options.sessionId, options.resume)) {
1418
1472
  labels.sessionId = options.sessionId;
1473
+ }
1419
1474
  // Only a launched pane sources-and-unlinks the env file. If createSession
1420
1475
  // throws, the pane never runs, so the resolved secrets (incl. the master
1421
1476
  // passphrase) would linger on disk — remove it on that failure path
@@ -1600,19 +1655,36 @@ async function spawnAgent(options) {
1600
1655
  command: executable,
1601
1656
  args: redactArgs(args.slice(0, 10)),
1602
1657
  });
1603
- // Interactive spawn-wrap: on macOS/Linux, run the agent INSIDE a shared-socket
1604
- // tmux session (then attach this TTY) so it gets a unique, addressable %pane.
1605
- // Headless runs, Windows, already-in-tmux, --raw, and AGENTS_NO_TMUX=1 keep the
1606
- // bare spawn below. See shouldWrapInTmux / runInTmux.
1607
- if (shouldWrapInTmux({
1658
+ // Interactive spawn-wrap: run the agent INSIDE a shared-socket tmux session
1659
+ // (then attach this TTY) so it gets a unique, addressable %pane — and, when
1660
+ // the run arrived over `--device`, so it survives the SSH link that carries
1661
+ // it. Every failed guard keeps the bare spawn below.
1662
+ const tmuxWrap = resolveTmuxWrap({
1608
1663
  interactive,
1609
1664
  platform: process.platform,
1610
1665
  inTmux: !!process.env.TMUX,
1611
1666
  raw: options.raw === true,
1612
1667
  noTmuxEnv: process.env.AGENTS_NO_TMUX === '1',
1613
1668
  configEnabled: isTmuxEnabled(),
1669
+ remoteDispatch: process.env[REMOTE_INTERACTIVE_ENV] === '1',
1614
1670
  tmuxAvailable: isTmuxInstalled(),
1615
- })) {
1671
+ });
1672
+ if (tmuxWrap.kind === 'undurable') {
1673
+ // Refuse rather than start work a blink would destroy. This is the ONLY
1674
+ // check — `ensureHostReady` gates a `--device` dispatch on the peer being
1675
+ // reachable and able to run the pinned agent, not on it having tmux, so
1676
+ // there is no launcher-side probe ahead of this one. Failing here still
1677
+ // fails before the agent starts, which is what matters; a pre-dispatch
1678
+ // probe would only move the message earlier, at the cost of a round trip on
1679
+ // every launch.
1680
+ const msg = `agents: ${machineId()} has no tmux, so a --device run here could not survive a dropped connection.\n`
1681
+ + ` install it: (apt|dnf|brew) install tmux\n`
1682
+ + ` or accept the risk: agents run … --device ${machineId()} --raw\n`;
1683
+ process.stderr.write(`\x1b[31m${msg}\x1b[0m`);
1684
+ timer.end({ exitCode: 1, status: 'failed', error: 'remote interactive run has no tmux for durability' });
1685
+ return { exitCode: 1, stdout: '', stderr: msg };
1686
+ }
1687
+ if (tmuxWrap.kind === 'wrap') {
1616
1688
  timer.mark('startup');
1617
1689
  try {
1618
1690
  const result = await runInTmux(options, executable, args);
@@ -0,0 +1,29 @@
1
+ import { type ActiveSession } from '../session/active.js';
2
+ import { type MessageReceipt } from './feed.js';
3
+ export interface VerifiedOperator {
4
+ id?: string;
5
+ verified: boolean;
6
+ label?: string;
7
+ }
8
+ export interface FeedAnswerResult {
9
+ status: 'delivered' | 'already_answered';
10
+ receipt: MessageReceipt;
11
+ }
12
+ /** Atomically claim the first answer, then route it over the session's recorded reply rail. */
13
+ export declare function claimAndRouteAttentionAnswer(input: {
14
+ attentionKey: string;
15
+ choiceId?: string;
16
+ text?: string;
17
+ operator: VerifiedOperator;
18
+ feedRoot?: string;
19
+ mailboxRoot?: string;
20
+ sessions?: ActiveSession[];
21
+ }): Promise<FeedAnswerResult>;
22
+ /** Forward a fleet attention answer to the device that owns its scope. */
23
+ export declare function forwardFeedAnswer(input: {
24
+ host: string;
25
+ attentionKey: string;
26
+ choiceId?: string;
27
+ text?: string;
28
+ operatorId?: string;
29
+ }): Promise<FeedAnswerResult>;