@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
@@ -67,6 +67,11 @@ export const DAEMON_SERVICES = [
67
67
  title: 'State-dir self-check',
68
68
  description: 'Self-terminates the daemon if its state directory is removed.',
69
69
  },
70
+ {
71
+ id: 'session-index',
72
+ title: 'Session-index warm',
73
+ description: 'Keeps this host\'s transcript index current so a locally-started session is discoverable within seconds.',
74
+ },
70
75
  ];
71
76
  /** Stable order of service IDs. */
72
77
  export const DAEMON_SERVICE_IDS = DAEMON_SERVICES.map((s) => s.id);
@@ -150,3 +155,53 @@ export function listDaemonServiceStates() {
150
155
  const cfg = readDaemonServicesConfig();
151
156
  return DAEMON_SERVICES.map((s) => ({ ...s, enabled: cfg.services[s.id] !== false }));
152
157
  }
158
+ /**
159
+ * A tiny cross-process action queue for `agents daemon services restart <id>`
160
+ * (RUSH-3193 P4) — SIGHUP can only signal "reload", it carries no payload, so a
161
+ * restart request is dropped here first and the daemon's reload handler drains
162
+ * it. Same file-backed pattern as `services.yaml` above, for the same reason:
163
+ * the CLI invocation and the daemon are separate processes.
164
+ */
165
+ function getDaemonServiceActionsPath() {
166
+ return path.join(getDaemonConfigDir(), 'service-actions.json');
167
+ }
168
+ /** Queue a live restart for `id`, to be picked up on the next SIGHUP reload. */
169
+ export function queueDaemonServiceRestart(id) {
170
+ const filePath = getDaemonServiceActionsPath();
171
+ let ids = [];
172
+ try {
173
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
174
+ if (Array.isArray(parsed.restart))
175
+ ids = parsed.restart;
176
+ }
177
+ catch {
178
+ // Missing or corrupt -> start a fresh queue.
179
+ }
180
+ if (!ids.includes(id))
181
+ ids.push(id);
182
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
183
+ atomicWriteFileSync(filePath, JSON.stringify({ restart: ids }), 'utf-8');
184
+ }
185
+ /**
186
+ * Drain every queued restart request. Called once per SIGHUP reload by the
187
+ * daemon; never throws. Clears the file so a request is applied at most once.
188
+ */
189
+ export function drainDaemonServiceRestartQueue() {
190
+ const filePath = getDaemonServiceActionsPath();
191
+ let ids = [];
192
+ try {
193
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
194
+ if (Array.isArray(parsed.restart))
195
+ ids = parsed.restart;
196
+ }
197
+ catch {
198
+ return [];
199
+ }
200
+ try {
201
+ fs.unlinkSync(filePath);
202
+ }
203
+ catch {
204
+ // Already gone.
205
+ }
206
+ return ids;
207
+ }
@@ -67,8 +67,9 @@ export declare function runFleetCacheWarmTick(): Promise<void>;
67
67
  /**
68
68
  * Usage refresh: keep the usage cache the `agents run` router reads
69
69
  * (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
70
- * The configured primary host is the fleet's sole provider-facing writer;
71
- * subscribers import its token-free derived cache.
70
+ * Every host is its own provider-facing writer — it refreshes only the
71
+ * accounts it holds credentials for, straight from the provider APIs
72
+ * (RUSH-3193 #15; no cross-host broadcast).
72
73
  */
73
74
  export declare function runUsageRefreshTick(): Promise<void>;
74
75
  /**
@@ -79,9 +80,19 @@ export declare function runUsageRefreshTick(): Promise<void>;
79
80
  * Cadence matches {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS} so one-shot readers and
80
81
  * long-lived watchers share one writer. Without this tick the journal has no
81
82
  * continuous producer and Factory freezes after the initial cache snapshot.
83
+ *
84
+ * Gated on reader presence (RUSH-3193): when no `sessions watch` / `feed watch`
85
+ * consumer has checked in within {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS} the
86
+ * expensive `ps`+`lsof` gather is skipped entirely on this scheduled tick. A
87
+ * watcher connecting to a cold/idle daemon is NOT served by this tick alone —
88
+ * {@link noteActiveSessionsJournalReader} only writes a presence timestamp, with
89
+ * no path back to this timer — so the daemon separately runs
90
+ * {@link watchActiveSessionsReaderPresence} to detect that idle→recent edge and
91
+ * fire an out-of-band call into this same function immediately (RUSH-2484).
82
92
  */
83
93
  export declare function runActiveSessionsWarmTick(opts?: {
84
94
  gather?: () => Promise<import('./session/active.js').ActiveSession[]>;
95
+ nowMs?: number;
85
96
  }): Promise<{
86
97
  sessions: number;
87
98
  }>;
@@ -89,7 +89,7 @@ export async function refreshLocalFleetAuthState(opts) {
89
89
  // Fleet status publishes every tick regardless — it does not ride that endpoint.
90
90
  const cached = readFleetAuthRows(self);
91
91
  const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
92
- const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion() });
92
+ const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force });
93
93
  if (!reuse)
94
94
  writeFleetAuthRows(self, authRows);
95
95
  const row = await publishLocalFleetStatus(self);
@@ -108,35 +108,23 @@ export async function runFleetCacheWarmTick() {
108
108
  /**
109
109
  * Usage refresh: keep the usage cache the `agents run` router reads
110
110
  * (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
111
- * The configured primary host is the fleet's sole provider-facing writer;
112
- * subscribers import its token-free derived cache.
111
+ * Every host is its own provider-facing writer — it refreshes only the
112
+ * accounts it holds credentials for, straight from the provider APIs
113
+ * (RUSH-3193 #15; no cross-host broadcast).
113
114
  */
114
115
  export async function runUsageRefreshTick() {
115
- const { resolveUsagePrimaryHost } = await import('./device-config.js');
116
- const { machineId } = await import('./machine-id.js');
117
- // usage.primary-host, else interactive.host, else null (standalone local refresh)
118
- const primaryHost = resolveUsagePrimaryHost() ?? undefined;
119
- const self = machineId();
120
- const { importUsageFleetFromHost, usageRefreshRole } = await import('./usage-fleet.js');
121
- if (usageRefreshRole(primaryHost, self) === 'subscriber') {
122
- const imported = await importUsageFleetFromHost(primaryHost);
123
- console.log(`usage refresh: imported ${Object.keys(imported.usage).length} account(s) from primary host ${primaryHost}`);
124
- return;
125
- }
126
116
  const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
127
117
  const { writeClaudeUsageCache } = await import('./accounting/usage.js');
128
118
  const { usageRateLimitedUntil } = await import('./usage-backoff.js');
129
119
  const r = await runUsageRefresh({
130
120
  listAccounts: buildLocalUsageAccounts,
131
121
  writeUsageCache: writeClaudeUsageCache,
132
- backoffUntil: usageRateLimitedUntil,
122
+ backoffUntil: (agentId, usageKey) => usageRateLimitedUntil(agentId, Date.now(), usageKey),
133
123
  });
134
124
  const { listProfiles } = await import('./profiles.js');
135
125
  const { refreshDueByokUsage } = await import('./byok-usage.js');
136
126
  const byok = await refreshDueByokUsage(listProfiles());
137
- const { exportUsageFleet } = await import('./usage-fleet.js');
138
- const published = exportUsageFleet();
139
- console.log(`usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped; BYOK ${byok.refreshed} refreshed, ${byok.skipped} not-due; published ${Object.keys(published.usage).length} account(s)`);
127
+ console.log(`usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped; BYOK ${byok.refreshed} refreshed, ${byok.skipped} not-due`);
140
128
  }
141
129
  /**
142
130
  * Active-sessions warm (RUSH-2062 / RUSH-2484): publish THIS host's live session
@@ -146,10 +134,24 @@ export async function runUsageRefreshTick() {
146
134
  * Cadence matches {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS} so one-shot readers and
147
135
  * long-lived watchers share one writer. Without this tick the journal has no
148
136
  * continuous producer and Factory freezes after the initial cache snapshot.
137
+ *
138
+ * Gated on reader presence (RUSH-3193): when no `sessions watch` / `feed watch`
139
+ * consumer has checked in within {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS} the
140
+ * expensive `ps`+`lsof` gather is skipped entirely on this scheduled tick. A
141
+ * watcher connecting to a cold/idle daemon is NOT served by this tick alone —
142
+ * {@link noteActiveSessionsJournalReader} only writes a presence timestamp, with
143
+ * no path back to this timer — so the daemon separately runs
144
+ * {@link watchActiveSessionsReaderPresence} to detect that idle→recent edge and
145
+ * fire an out-of-band call into this same function immediately (RUSH-2484).
149
146
  */
150
147
  export async function runActiveSessionsWarmTick(opts = {}) {
151
- const { publishLocalActiveSessions } = await import('./session/session-cache.js');
152
- const r = await publishLocalActiveSessions({ gather: opts.gather });
148
+ const { publishLocalActiveSessions, isActiveSessionsJournalReaderRecent } = await import('./session/session-cache.js');
149
+ const nowMs = opts.nowMs ?? Date.now();
150
+ if (!isActiveSessionsJournalReaderRecent(nowMs)) {
151
+ console.log('active-sessions warm: idle (no recent reader), skipping gather');
152
+ return { sessions: 0 };
153
+ }
154
+ const r = await publishLocalActiveSessions({ gather: opts.gather, nowMs });
153
155
  console.log(`active-sessions warm: ${r.sessions.length} session(s) published`);
154
156
  return { sessions: r.sessions.length };
155
157
  }
@@ -124,8 +124,6 @@ export declare function listConfig(opts?: ConfigTarget): ConfigEntry[];
124
124
  /** List user-scope config keys with their values. Used to show inherited settings
125
125
  * in per-device views without implying those keys are device-local. */
126
126
  export declare function listUserConfig(): ConfigEntry[];
127
- /** Resolve the explicit usage host, falling back to the user's interactive host. */
128
- export declare function resolveUsagePrimaryHost(): string | null;
129
127
  /**
130
128
  * Set a config key (validated). Device-scope keys target this machine unless
131
129
  * `opts.device` names a peer; `opts.fleet` writes the fleet-wide defaults layer
@@ -147,8 +145,32 @@ export type AutoPoolMode = (typeof AUTO_POOL_MODES)[number];
147
145
  * explicit `worker` mark (see {@link listConfiguredDeviceRoles}).
148
146
  */
149
147
  export declare function configuredDeviceRole(name: string): ConfiguredDeviceRole | undefined;
148
+ /**
149
+ * The role marked on THIS machine — the one running the CLI — or undefined when
150
+ * it was never marked. Keyed off {@link machineId} (overridable via
151
+ * AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
152
+ *
153
+ * The auth strategy reads this: a `personal` device (the user's own interactive
154
+ * box) holds a real per-version login and MUST authenticate from it for EVERY
155
+ * run, interactive or headless; only a `worker` uses the file-based setup-token
156
+ * (RUSH-2395). `undefined` is treated as non-personal (worker-equivalent) by
157
+ * that gate — an unmarked box has no login to defer to.
158
+ */
159
+ export declare function selfConfiguredDeviceRole(): ConfiguredDeviceRole | undefined;
150
160
  /** Mark a device's role fleet-wide; `undefined` clears the mark. */
151
161
  export declare function setConfiguredDeviceRole(name: string, role: ConfiguredDeviceRole | undefined): void;
162
+ /**
163
+ * Devices whose OWN config pins one of the browser profile keys to `profile`.
164
+ *
165
+ * Used by `profiles rename` to warn rather than silently leave a peer pointing at
166
+ * a name that no longer exists. Deliberately read-only: rewriting another
167
+ * machine's device doc from here would be a cross-machine mutation nobody asked
168
+ * for, and the pin may well be correct there (a local profile of the same name).
169
+ */
170
+ export declare function devicesPinningBrowserProfile(profile: string): Array<{
171
+ device: string;
172
+ key: 'browser.profile' | 'browser.viewer';
173
+ }>;
152
174
  /**
153
175
  * Every device with an effective role, keyed by device name. Layers like every
154
176
  * other device-scope key: the fleet default (central fleet.defaults.config)
@@ -194,14 +216,7 @@ export declare function isSchedulerEnabled(): boolean;
194
216
  * scheduler init) refuses with.
195
217
  */
196
218
  export declare function assertSchedulerEnabled(): void;
197
- /**
198
- * True unless this machine's config turns off the managed tmux wrap for
199
- * interactive `agents run` launches (`tmux.enabled=false`).
200
- *
201
- * Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
202
- * per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
203
- * broken or unwanted. Unset means today's behavior: wrap.
204
- */
219
+ /** True only when this machine explicitly enables the managed tmux wrap. */
205
220
  export declare function isTmuxEnabled(): boolean;
206
221
  /** True unless this machine's config disables the daemon outright (top-level kill switch). */
207
222
  export declare function isDaemonEnabled(): boolean;
@@ -36,7 +36,7 @@ import * as yaml from 'yaml';
36
36
  import { META_HEADER, getUserAgentsDir, readMeta, updateMeta, withMetaLock } from './state.js';
37
37
  import { atomicWriteFileSync } from './fs-atomic.js';
38
38
  import { machineId } from './machine-id.js';
39
- import { assertValidDeviceName } from './devices/registry.js';
39
+ import { assertValidDeviceName, assertRegistrableDeviceName } from './devices/registry.js';
40
40
  import { migrateDeviceConfigStores } from './devices/config-migration.js';
41
41
  const DEVICE_PLATFORMS = ['windows', 'linux', 'macos', 'unknown'];
42
42
  const SSH_AUTH_METHODS = ['key', 'password'];
@@ -53,23 +53,7 @@ export const CONFIG_KEYS = [
53
53
  description: 'Device that shows the user artifacts (browser opens, dashboards) — the "online macOS box" skills should use instead of guessing.',
54
54
  validate: (v) => {
55
55
  try {
56
- assertValidDeviceName(v);
57
- return null;
58
- }
59
- catch (err) {
60
- return err?.message ?? String(err);
61
- }
62
- },
63
- },
64
- {
65
- name: 'usage.primary-host',
66
- yamlKey: 'usagePrimaryHost',
67
- scope: 'user',
68
- type: 'string',
69
- description: 'Device whose usage snapshots are authoritative for fleet-wide usage reporting.',
70
- validate: (v) => {
71
- try {
72
- assertValidDeviceName(v);
56
+ assertRegistrableDeviceName(v);
73
57
  return null;
74
58
  }
75
59
  catch (err) {
@@ -90,6 +74,18 @@ export const CONFIG_KEYS = [
90
74
  ? null
91
75
  : `auto.pool must be one of ${AUTO_POOL_MODES.join(' | ')}.`,
92
76
  },
77
+ {
78
+ name: 'browser.viewer',
79
+ yamlKey: 'browserViewer',
80
+ scope: 'device',
81
+ visibility: 'machine',
82
+ type: 'string',
83
+ description: "Which browser THIS machine shows YOU a page in (an .html artifact, `agents feedback`, a login " +
84
+ "dashboard): a profile name, or `os` for the OS default handler. Unset follows browser.profile. " +
85
+ "Distinct from browser.profile, which is the profile agents DRIVE — see RUSH-2709 for why " +
86
+ "collapsing the two was a mistake. Set it to `os` to keep the OS default handler.",
87
+ validate: (v) => typeof v === 'string' && v.length > 0 ? null : 'browser.viewer must be `os` or a profile name.',
88
+ },
93
89
  {
94
90
  name: 'browser.profile',
95
91
  yamlKey: 'defaultBrowserProfile',
@@ -144,11 +140,10 @@ export const CONFIG_KEYS = [
144
140
  scope: 'device',
145
141
  visibility: 'machine',
146
142
  type: 'bool',
147
- defaultValue: true,
143
+ defaultValue: false,
148
144
  description: 'Whether an interactive `agents run` on this device is wrapped in the shared-socket tmux session. ' +
149
- 'On gives every agent an addressable pane (`agents sessions --active` tells co-located agents apart, ' +
150
- '`agents focus` re-attaches without forking). Off spawns the agent directly on this box the durable ' +
151
- 'form of `--no-tmux`, for a machine whose tmux is broken or unwanted.',
145
+ 'Off, the default, spawns the agent directly. Turn it on to give every agent an addressable pane for ' +
146
+ '`agents message`, injection, and `agents focus` once the tmux mouse, clipboard, and scrollback behavior suits this device.',
152
147
  },
153
148
  {
154
149
  name: 'browser.remote-control',
@@ -170,7 +165,7 @@ export const CONFIG_KEYS = [
170
165
  description: 'Minutes a browser task may sit with no IPC action (navigate, click, type, screenshot, …) before the daemon\'s ' +
171
166
  'abandoned-task reaper closes its tabs and marks it done (RUSH-2622). 0 disables idle reaping — the reaper still ' +
172
167
  "closes a task whose owning agent session has exited, whatever this is set to. Read only on THIS box's own " +
173
- 'reaper tick and `agents browser gc`, so it never applies to a peer.',
168
+ 'reaper tick and `agents browser prune`, so it never applies to a peer.',
174
169
  validate: (v) => v >= 0 ? null : 'browser.task-idle-minutes must be >= 0 (0 disables idle reaping).',
175
170
  },
176
171
  {
@@ -179,7 +174,26 @@ export const CONFIG_KEYS = [
179
174
  scope: 'device',
180
175
  visibility: 'shared',
181
176
  type: 'string-list',
182
- description: 'Free-form operator notes about this device (one entry per `agents devices config <name> notes <text>`).',
177
+ description: 'Free-form operator notes about this device (one entry per `agents devices config <name> notes <text>`). ' +
178
+ 'Long-form scratch, never shown in device listings — for the one-line synced summary of what the box is for, use `description`.',
179
+ },
180
+ {
181
+ name: 'description',
182
+ yamlKey: 'description',
183
+ scope: 'device',
184
+ visibility: 'shared',
185
+ type: 'string',
186
+ description: 'One line saying what this device is FOR ("gpu box — cuda 12.4"), synced fleet-wide. Kept to one short line ' +
187
+ 'because the device-list renderer will show it (RUSH-3062, `surface` track). ' +
188
+ 'Replaces the value on each set; for appended long-form scratch use `notes`.',
189
+ validate: (v) => {
190
+ const s = v;
191
+ if (s.includes('\n') || s.includes('\r'))
192
+ return 'description must be a single line.';
193
+ if (s.length > 80)
194
+ return `description must be at most 80 characters (got ${s.length}) — it is a one-line table cell.`;
195
+ return null;
196
+ },
183
197
  },
184
198
  {
185
199
  name: 'ssh.user',
@@ -456,12 +470,6 @@ export function listConfig(opts) {
456
470
  export function listUserConfig() {
457
471
  return CONFIG_KEYS.filter((spec) => spec.scope === 'user').map((spec) => getConfigValue(spec.name));
458
472
  }
459
- /** Resolve the explicit usage host, falling back to the user's interactive host. */
460
- export function resolveUsagePrimaryHost() {
461
- return getConfigValue('usage.primary-host').value
462
- ?? getConfigValue('interactive.host').value
463
- ?? null;
464
- }
465
473
  // ─── Writes ───────────────────────────────────────────────────────────────────
466
474
  /** The fleet manifest for a defaults write: `devices` materializes as an
467
475
  * explicit empty map (NOT 'all') so `agents apply` targets nothing until the
@@ -593,6 +601,20 @@ export function configuredDeviceRole(name) {
593
601
  assertValidDeviceName(name);
594
602
  return getConfigValue('role', { device: name }).value;
595
603
  }
604
+ /**
605
+ * The role marked on THIS machine — the one running the CLI — or undefined when
606
+ * it was never marked. Keyed off {@link machineId} (overridable via
607
+ * AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
608
+ *
609
+ * The auth strategy reads this: a `personal` device (the user's own interactive
610
+ * box) holds a real per-version login and MUST authenticate from it for EVERY
611
+ * run, interactive or headless; only a `worker` uses the file-based setup-token
612
+ * (RUSH-2395). `undefined` is treated as non-personal (worker-equivalent) by
613
+ * that gate — an unmarked box has no login to defer to.
614
+ */
615
+ export function selfConfiguredDeviceRole() {
616
+ return configuredDeviceRole(machineId());
617
+ }
596
618
  /** Mark a device's role fleet-wide; `undefined` clears the mark. */
597
619
  export function setConfiguredDeviceRole(name, role) {
598
620
  assertValidDeviceName(name);
@@ -601,6 +623,42 @@ export function setConfiguredDeviceRole(name, role) {
601
623
  else
602
624
  setConfigValue('role', role, { device: name });
603
625
  }
626
+ /**
627
+ * Devices whose OWN config pins one of the browser profile keys to `profile`.
628
+ *
629
+ * Used by `profiles rename` to warn rather than silently leave a peer pointing at
630
+ * a name that no longer exists. Deliberately read-only: rewriting another
631
+ * machine's device doc from here would be a cross-machine mutation nobody asked
632
+ * for, and the pin may well be correct there (a local profile of the same name).
633
+ */
634
+ export function devicesPinningBrowserProfile(profile) {
635
+ ensureDeviceConfigMigrated();
636
+ // Reports WHICH key each device used, not just that it matched. A caller
637
+ // telling the user to fix `browser.profile` on a device that actually pinned
638
+ // `browser.viewer` leaves the real pin broken and sets a second key nobody
639
+ // asked for.
640
+ const hits = [];
641
+ const devicesRoot = path.join(getUserAgentsDir(), 'devices');
642
+ let names = [];
643
+ try {
644
+ names = fs
645
+ .readdirSync(devicesRoot, { withFileTypes: true })
646
+ .filter((e) => e.isDirectory())
647
+ .map((e) => e.name);
648
+ }
649
+ catch {
650
+ return hits;
651
+ }
652
+ for (const name of names.sort()) {
653
+ const cfg = readDeviceDocConfig(name);
654
+ // A device can pin BOTH; each needs its own fix line.
655
+ if (cfg.defaultBrowserProfile === profile)
656
+ hits.push({ device: name, key: 'browser.profile' });
657
+ if (cfg.browserViewer === profile)
658
+ hits.push({ device: name, key: 'browser.viewer' });
659
+ }
660
+ return hits;
661
+ }
604
662
  /**
605
663
  * Every device with an effective role, keyed by device name. Layers like every
606
664
  * other device-scope key: the fleet default (central fleet.defaults.config)
@@ -720,16 +778,9 @@ export function assertSchedulerEnabled() {
720
778
  throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
721
779
  `Re-enable with: agents devices config ${machineId()} scheduler.enabled on`);
722
780
  }
723
- /**
724
- * True unless this machine's config turns off the managed tmux wrap for
725
- * interactive `agents run` launches (`tmux.enabled=false`).
726
- *
727
- * Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
728
- * per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
729
- * broken or unwanted. Unset means today's behavior: wrap.
730
- */
781
+ /** True only when this machine explicitly enables the managed tmux wrap. */
731
782
  export function isTmuxEnabled() {
732
- return getConfigValue('tmux.enabled').value !== false;
783
+ return getConfigValue('tmux.enabled').value === true;
733
784
  }
734
785
  /** True unless this machine's config disables the daemon outright (top-level kill switch). */
735
786
  export function isDaemonEnabled() {
@@ -13,6 +13,13 @@
13
13
  * `.history/devices/pins-<host>.json` (pins file wins on conflict — it
14
14
  * is the destination); peers' pins are simply dropped from the tracked
15
15
  * file (each peer owns/rewrites its own pins locally).
16
+ * (e) legacy `.history/devices/ignored.json` (per-machine, UNTRACKED — a
17
+ * dismissal never reached the rest of the fleet) folds into the central
18
+ * TRACKED `fleet.ignored` list (RUSH-3062). Entries keep the legacy
19
+ * file's `updatedAt` as `ignoredAt` and take THIS machine's id as
20
+ * `ignoredOn` — the legacy store recorded neither, so the folding box
21
+ * is the only attribution available. The legacy file is removed only
22
+ * AFTER the central write lands.
16
23
  *
17
24
  * What stays put: a device doc's existing `config:` (already the right home)
18
25
  * and its `routines:` list (operator-owned, read cross-device by
@@ -13,6 +13,13 @@
13
13
  * `.history/devices/pins-<host>.json` (pins file wins on conflict — it
14
14
  * is the destination); peers' pins are simply dropped from the tracked
15
15
  * file (each peer owns/rewrites its own pins locally).
16
+ * (e) legacy `.history/devices/ignored.json` (per-machine, UNTRACKED — a
17
+ * dismissal never reached the rest of the fleet) folds into the central
18
+ * TRACKED `fleet.ignored` list (RUSH-3062). Entries keep the legacy
19
+ * file's `updatedAt` as `ignoredAt` and take THIS machine's id as
20
+ * `ignoredOn` — the legacy store recorded neither, so the folding box
21
+ * is the only attribution available. The legacy file is removed only
22
+ * AFTER the central write lands.
16
23
  *
17
24
  * What stays put: a device doc's existing `config:` (already the right home)
18
25
  * and its `routines:` list (operator-owned, read cross-device by
@@ -33,9 +40,10 @@
33
40
  import * as fs from 'fs';
34
41
  import * as path from 'path';
35
42
  import * as yaml from 'yaml';
36
- import { META_HEADER, getDevicesAutoLaunchPath, getDevicePinsPath, getUserAgentsDir, readMeta, updateMeta, withMetaLock, } from '../state.js';
43
+ import { META_HEADER, getDevicesAutoLaunchPath, getDevicesIgnoredPath, getDevicePinsPath, getUserAgentsDir, readMeta, updateMeta, withMetaLock, } from '../state.js';
37
44
  import { atomicWriteFileSync } from '../fs-atomic.js';
38
45
  import { machineId } from '../machine-id.js';
46
+ import { withIgnoredAdded } from './registry.js';
39
47
  /** Read every device doc. A doc that fails to parse is loudly skipped. */
40
48
  function readDeviceDocs(devicesRoot) {
41
49
  const docs = [];
@@ -124,6 +132,32 @@ function readAutoLaunchFlags(autoLaunchPath) {
124
132
  function isConfigMap(v) {
125
133
  return typeof v === 'object' && v !== null && !Array.isArray(v);
126
134
  }
135
+ /**
136
+ * The legacy per-machine ignored.json. Absent => null. A parse failure is
137
+ * logged loudly and reported as `undefined` so the caller LEAVES the file in
138
+ * place for a later retry — never silently emptied, the same corruption
139
+ * contract the ignore-list itself keeps (registry.ts's loadIgnoredEntries).
140
+ */
141
+ function readLegacyIgnoredFile(p) {
142
+ let raw;
143
+ try {
144
+ raw = fs.readFileSync(p, 'utf-8');
145
+ }
146
+ catch {
147
+ return null; // absent — nothing to fold
148
+ }
149
+ try {
150
+ const parsed = JSON.parse(raw);
151
+ return {
152
+ names: Array.isArray(parsed.ignored) ? parsed.ignored.filter((n) => typeof n === 'string') : [],
153
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : undefined,
154
+ };
155
+ }
156
+ catch (err) {
157
+ console.error(`device config migration: could not parse ${p} (${err.message}); leaving it for a later retry`);
158
+ return undefined;
159
+ }
160
+ }
127
161
  /**
128
162
  * Fold every legacy device-config/pins location into the current layout. Safe
129
163
  * to call on every boot / config access: cheap no-op once folded.
@@ -139,6 +173,9 @@ export function migrateDeviceConfigStores() {
139
173
  const centralHasConfig = Object.values(centralDevices).some((ov) => ov?.config && Object.keys(ov.config).length > 0);
140
174
  const autoLaunchFlags = readAutoLaunchFlags(autoLaunchPath);
141
175
  const docs = readDeviceDocs(devicesRoot);
176
+ const legacyIgnoredPath = getDevicesIgnoredPath();
177
+ const legacyIgnoredPending = fs.existsSync(legacyIgnoredPath);
178
+ const legacyIgnored = legacyIgnoredPending ? readLegacyIgnoredFile(legacyIgnoredPath) : null;
142
179
  const pinsPath = getDevicePinsPath();
143
180
  let selfPins = {};
144
181
  try {
@@ -172,7 +209,7 @@ export function migrateDeviceConfigStores() {
172
209
  : undefined;
173
210
  const hasDestinationWork = plans.length > 0 || newDocs.length > 0 || docAgents !== undefined || docIsolated !== undefined;
174
211
  const autoLaunchPending = fs.existsSync(autoLaunchPath);
175
- if (!centralHasConfig && !hasDestinationWork && !autoLaunchPending)
212
+ if (!centralHasConfig && !hasDestinationWork && !autoLaunchPending && !legacyIgnoredPending)
176
213
  return;
177
214
  // ── 2. Destination writes FIRST (crash-safe), under the meta lock so they
178
215
  // serialize against writeMetaUnlocked's own read-merge-write of the doc.
@@ -221,7 +258,18 @@ export function migrateDeviceConfigStores() {
221
258
  nextDevices[name] = rest;
222
259
  }
223
260
  const fleet = { ...m.fleet, devices: nextDevices };
224
- if (Object.keys(nextDevices).length === 0 && !fleet.defaults && !fleet.secrets && !fleet.routines) {
261
+ // Drop the whole `fleet` block only when NOTHING else lives in it. Every
262
+ // resident must be named here: `ignored` holds the user's dismissals and
263
+ // `discovery` their discovery policy, and deleting the block would not
264
+ // just lose them locally — `agents repo push` would sync the deletion
265
+ // fleet-wide. Add any new fleet.* key to this guard.
266
+ const fleetIsEmpty = Object.keys(nextDevices).length === 0 &&
267
+ !fleet.defaults &&
268
+ !fleet.secrets &&
269
+ !fleet.routines &&
270
+ !fleet.discovery &&
271
+ !(fleet.ignored && fleet.ignored.length > 0);
272
+ if (fleetIsEmpty) {
225
273
  const { fleet: _, ...rest } = m;
226
274
  void _;
227
275
  return rest;
@@ -238,6 +286,22 @@ export function migrateDeviceConfigStores() {
238
286
  console.error(`device config migration: could not remove ${autoLaunchPath} (${err.message}); a later run retries`);
239
287
  }
240
288
  }
289
+ // ── 4. Legacy ignored.json → central fleet.ignored ───────────────────────
290
+ // Destination write (updateMeta: withMetaLock + atomic write) lands BEFORE
291
+ // the legacy file is removed, so a crash re-folds on the next run and
292
+ // withIgnoredAdded's union-by-name makes that a no-op. A file that failed to
293
+ // parse (legacyIgnored === undefined) is left in place for that retry.
294
+ if (legacyIgnored) {
295
+ if (legacyIgnored.names.length > 0) {
296
+ updateMeta((m) => withIgnoredAdded(m, legacyIgnored.names, legacyIgnored.updatedAt ?? new Date().toISOString()));
297
+ }
298
+ try {
299
+ fs.rmSync(legacyIgnoredPath, { force: true });
300
+ }
301
+ catch (err) {
302
+ console.error(`device config migration: could not remove ${legacyIgnoredPath} (${err.message}); a later run retries`);
303
+ }
304
+ }
241
305
  }
242
306
  /** Shallow-set equality for folded doc content (values compared deep via JSON). */
243
307
  function sameDocContent(a, b) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Device resource probing for `agents devices list`.
3
3
  *
4
- * One SSH round-trip per device gathers load average, memory pressure, and core
4
+ * One SSH round-trip per device gathers load average, memory pressure, disk, and core
5
5
  * count (mac + linux via a POSIX snippet, windows via a CIM one-liner), parsed
6
6
  * into a {@link DeviceStats}. Probes run in parallel with a bounded timeout so
7
7
  * the list stays responsive — a slow or hung box degrades to "no stats" instead
@@ -19,6 +19,19 @@ export declare const PROBE_TIMEOUT_MS = 2500;
19
19
  * "Preparing modules for first use" cost on top of PowerShell startup, which
20
20
  * routinely blows the 2.5s POSIX budget on a relayed connection. */
21
21
  export declare const WIN_PROBE_TIMEOUT_MS = 6000;
22
+ /** One-shot remote snapshot: load, memory, core count, then root filesystem. */
23
+ export declare const PROBE_SNIPPET = "uptime; echo ---AGSTAT---; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ---AGSTAT---; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null); echo ---AGSTAT---; df -Pk / 2>/dev/null | tail -1";
24
+ /** Windows equivalent, one labeled line via CIM. PowerShell 5.1-safe: no `||`
25
+ * chaining, plain string concatenation. `LoadPercentage` is $null on some
26
+ * hosts/VMs, which concatenates to an empty field — the parser treats that as
27
+ * "no load signal" and headroom falls back to memory pressure alone.
28
+ * wrapRemoteCommand base64-encodes this for powershell-shell devices, so the
29
+ * quoting survives ssh intact. */
30
+ export declare const WIN_PROBE_SNIPPET = "$os = Get-CimInstance Win32_OperatingSystem; $cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; $disk = Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='C:'\"; Write-Output ('AGWINSTAT load=' + $cpu + ' freeKb=' + $os.FreePhysicalMemory + ' totalKb=' + $os.TotalVisibleMemorySize + ' ncpu=' + $env:NUMBER_OF_PROCESSORS + ' diskFreeKb=' + ($disk.FreeSpace / 1KB) + ' diskTotalKb=' + ($disk.Size / 1KB))";
31
+ export declare function localProbeInvocation(platform: NodeJS.Platform): {
32
+ file: string;
33
+ args: string[];
34
+ };
22
35
  export interface DeviceStats {
23
36
  host: string;
24
37
  reachable: boolean;
@@ -30,6 +43,11 @@ export interface DeviceStats {
30
43
  memPercent?: number;
31
44
  memTotalBytes?: number;
32
45
  memFreeBytes?: number;
46
+ diskTotalBytes?: number;
47
+ diskFreeBytes?: number;
48
+ diskUsedPercent?: number;
49
+ /** When the static ncpu/RAM/disk totals were last observed. */
50
+ specsFetchedAt?: number;
33
51
  fetchedAt: number;
34
52
  }
35
53
  /** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
@@ -47,7 +65,14 @@ export declare function parseLinuxMemInfo(out: string): MemStats;
47
65
  export declare function parseNcpu(out: string): {
48
66
  ncpu?: number;
49
67
  };
50
- /** Assemble a DeviceStats from the three snippet sections. */
68
+ interface DiskStats {
69
+ diskTotalBytes?: number;
70
+ diskFreeBytes?: number;
71
+ diskUsedPercent?: number;
72
+ }
73
+ /** Parse the final data row from `df -k /`, deriving usage from block counts. */
74
+ export declare function parseDf(out: string): DiskStats;
75
+ /** Assemble a DeviceStats from the four snippet sections. */
51
76
  export declare function parseProbeOutput(host: string, stdout: string, fetchedAt: number): DeviceStats;
52
77
  /** Assemble a DeviceStats from the windows one-liner. A missing marker line
53
78
  * (e.g. CIM unavailable) keeps `reachable: true` — ssh answered — with no