@phnx-labs/agents-cli 1.22.117 → 1.22.119

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 (271) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/accounts.js +7 -4
  4. package/dist/commands/browser.d.ts +52 -70
  5. package/dist/commands/browser.js +192 -3562
  6. package/dist/commands/daemon.d.ts +39 -2
  7. package/dist/commands/daemon.js +72 -73
  8. package/dist/commands/events.js +1 -1
  9. package/dist/commands/exec.js +53 -1
  10. package/dist/commands/projects.js +92 -7
  11. package/dist/commands/repo.js +13 -49
  12. package/dist/commands/route.js +5 -5
  13. package/dist/commands/routines.js +39 -12
  14. package/dist/commands/sessions-render.d.ts +1 -1
  15. package/dist/commands/sessions-render.js +0 -1
  16. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  17. package/dist/commands/sessions.test-fixture.js +0 -37
  18. package/dist/commands/setup-browser.d.ts +17 -12
  19. package/dist/commands/setup-browser.js +52 -87
  20. package/dist/commands/setup-preferences.d.ts +0 -17
  21. package/dist/commands/setup-preferences.js +18 -49
  22. package/dist/commands/setup.js +9 -9
  23. package/dist/commands/ssh.js +10 -1
  24. package/dist/commands/sync.js +6 -44
  25. package/dist/commands/teams.js +0 -1
  26. package/dist/commands/update.d.ts +19 -0
  27. package/dist/commands/update.js +34 -6
  28. package/dist/commands/view.d.ts +12 -1
  29. package/dist/commands/view.js +47 -2
  30. package/dist/index.js +107 -26
  31. package/dist/lib/account-capabilities.js +0 -2
  32. package/dist/lib/account-catalog.d.ts +38 -0
  33. package/dist/lib/account-catalog.js +110 -26
  34. package/dist/lib/account-provider-registry.js +1 -1
  35. package/dist/lib/account-registry.d.ts +1 -1
  36. package/dist/lib/account-registry.js +36 -2
  37. package/dist/lib/accounting/rotate.js +22 -8
  38. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  39. package/dist/lib/accounting/usage-ingest.js +115 -45
  40. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  41. package/dist/lib/accounting/usage-sync.js +227 -66
  42. package/dist/lib/accounting/usage.d.ts +16 -3
  43. package/dist/lib/accounting/usage.js +26 -8
  44. package/dist/lib/accounts/add.d.ts +3 -1
  45. package/dist/lib/accounts/add.js +24 -13
  46. package/dist/lib/accounts/slots.d.ts +8 -0
  47. package/dist/lib/accounts/slots.js +17 -0
  48. package/dist/lib/add-dir.js +0 -1
  49. package/dist/lib/agent-cli-commands.js +0 -1
  50. package/dist/lib/agent-spec/agents.d.ts +6 -7
  51. package/dist/lib/agent-spec/agents.js +7 -60
  52. package/dist/lib/auth-health.d.ts +69 -1
  53. package/dist/lib/auth-health.js +133 -4
  54. package/dist/lib/browser/context.d.ts +82 -0
  55. package/dist/lib/browser/context.js +88 -0
  56. package/dist/lib/browser/paths.d.ts +25 -0
  57. package/dist/lib/browser/paths.js +51 -0
  58. package/dist/lib/browser/record.d.ts +38 -0
  59. package/dist/lib/browser/record.js +57 -0
  60. package/dist/lib/browser/sessions-list.js +1 -2
  61. package/dist/lib/browser-client.d.ts +158 -0
  62. package/dist/lib/browser-client.js +234 -0
  63. package/dist/lib/channels/owner-sink.d.ts +1 -18
  64. package/dist/lib/channels/owner-sink.js +29 -82
  65. package/dist/lib/channels/providers/rush.d.ts +7 -7
  66. package/dist/lib/channels/providers/rush.js +127 -57
  67. package/dist/lib/claude-account-token.d.ts +7 -1
  68. package/dist/lib/claude-account-token.js +26 -4
  69. package/dist/lib/claude-statusline.d.ts +1 -1
  70. package/dist/lib/claude-statusline.js +15 -3
  71. package/dist/lib/commands.js +1 -1
  72. package/dist/lib/crabbox/lease.js +1 -1
  73. package/dist/lib/crabbox/runtimes.js +1 -3
  74. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  75. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  76. package/dist/lib/daemon/auth-sync-service.js +37 -43
  77. package/dist/lib/daemon/daemon.d.ts +25 -14
  78. package/dist/lib/daemon/daemon.js +92 -154
  79. package/dist/lib/daemon/runner.d.ts +2 -2
  80. package/dist/lib/daemon/runner.js +32 -9
  81. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  82. package/dist/lib/daemon/self-update-service.js +40 -36
  83. package/dist/lib/daemon/service.d.ts +12 -8
  84. package/dist/lib/daemon/supervisor.d.ts +65 -54
  85. package/dist/lib/daemon/supervisor.js +136 -171
  86. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  87. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  88. package/dist/lib/daemon/usage-sync-service.js +46 -78
  89. package/dist/lib/daemon-health.d.ts +22 -5
  90. package/dist/lib/daemon-health.js +47 -3
  91. package/dist/lib/daemon-services.d.ts +1 -1
  92. package/dist/lib/daemon-services.js +0 -10
  93. package/dist/lib/daemon-ticks.js +4 -2
  94. package/dist/lib/devices/doctor-findings.js +8 -11
  95. package/dist/lib/devices/harness-inventory.js +14 -5
  96. package/dist/lib/exec-account-home.js +5 -0
  97. package/dist/lib/exec.js +10 -13
  98. package/dist/lib/feed/events.js +0 -1
  99. package/dist/lib/feed/tool-activity.js +5 -2
  100. package/dist/lib/fleet-shared-state.d.ts +45 -0
  101. package/dist/lib/fleet-shared-state.js +78 -9
  102. package/dist/lib/fs-atomic.js +25 -0
  103. package/dist/lib/github/pr-mergeable.d.ts +4 -2
  104. package/dist/lib/github/pr-mergeable.js +5 -3
  105. package/dist/lib/github/project-prs.d.ts +58 -3
  106. package/dist/lib/github/project-prs.js +205 -7
  107. package/dist/lib/harness/adapters/cursor.js +14 -4
  108. package/dist/lib/harness-auth-capabilities.d.ts +4 -3
  109. package/dist/lib/harness-auth-capabilities.js +1 -2
  110. package/dist/lib/helper-versions.js +1 -1
  111. package/dist/lib/hooks/cache.d.ts +7 -0
  112. package/dist/lib/hooks/cache.js +27 -2
  113. package/dist/lib/hooks/install.js +7 -1
  114. package/dist/lib/hosts/ready.d.ts +38 -8
  115. package/dist/lib/hosts/ready.js +62 -14
  116. package/dist/lib/installations/active-check.d.ts +27 -0
  117. package/dist/lib/installations/active-check.js +61 -0
  118. package/dist/lib/installations/migrate.js +6 -3
  119. package/dist/lib/installations/shims.d.ts +1 -1
  120. package/dist/lib/installations/shims.js +32 -12
  121. package/dist/lib/installations/store.d.ts +9 -0
  122. package/dist/lib/installations/store.js +24 -0
  123. package/dist/lib/installations/update.js +21 -15
  124. package/dist/lib/installations/versions.js +15 -3
  125. package/dist/lib/manifest.js +1 -1
  126. package/dist/lib/models.js +0 -2
  127. package/dist/lib/open-url.js +28 -54
  128. package/dist/lib/profiles.js +0 -2
  129. package/dist/lib/projects.d.ts +16 -0
  130. package/dist/lib/projects.js +31 -0
  131. package/dist/lib/refresh.js +1 -1
  132. package/dist/lib/routine-readiness.d.ts +35 -0
  133. package/dist/lib/routine-readiness.js +74 -16
  134. package/dist/lib/rules/compile.js +6 -0
  135. package/dist/lib/sandbox.js +0 -2
  136. package/dist/lib/secrets-policy.d.ts +89 -11
  137. package/dist/lib/secrets-policy.js +175 -28
  138. package/dist/lib/session/active.d.ts +17 -6
  139. package/dist/lib/session/active.js +105 -49
  140. package/dist/lib/session/db.d.ts +1 -1
  141. package/dist/lib/session/db.js +65 -18
  142. package/dist/lib/session/discover.js +1 -283
  143. package/dist/lib/session/mirror.d.ts +4 -3
  144. package/dist/lib/session/mirror.js +4 -3
  145. package/dist/lib/session/parse.d.ts +0 -2
  146. package/dist/lib/session/parse.js +1 -138
  147. package/dist/lib/session/pid-registry.d.ts +6 -3
  148. package/dist/lib/session/pid-registry.js +18 -8
  149. package/dist/lib/session/remote/watch.d.ts +1 -1
  150. package/dist/lib/session/remote/watch.js +7 -3
  151. package/dist/lib/session/throughput.d.ts +1 -3
  152. package/dist/lib/session/throughput.js +0 -20
  153. package/dist/lib/session/types.d.ts +2 -2
  154. package/dist/lib/session/types.js +2 -2
  155. package/dist/lib/sessions-client.d.ts +60 -1
  156. package/dist/lib/sessions-client.js +227 -4
  157. package/dist/lib/setup-tool-status.js +5 -10
  158. package/dist/lib/signin-badge.d.ts +3 -2
  159. package/dist/lib/signin-badge.js +6 -3
  160. package/dist/lib/smart-launch.js +4 -1
  161. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  162. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  163. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  164. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  165. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  166. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  167. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  168. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  169. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  170. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  171. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  172. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  173. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  174. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  175. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  176. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  177. package/dist/lib/staleness/writers/sources.js +5 -0
  178. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  179. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  180. package/dist/lib/state.d.ts +6 -4
  181. package/dist/lib/state.js +7 -17
  182. package/dist/lib/teams/parsers.d.ts +1 -1
  183. package/dist/lib/teams/parsers.js +1 -136
  184. package/dist/lib/teams/placement-probe.js +50 -15
  185. package/dist/lib/teams/scheduler.d.ts +7 -0
  186. package/dist/lib/types.d.ts +16 -1
  187. package/dist/lib/types.js +1 -1
  188. package/dist/lib/view-types.d.ts +32 -0
  189. package/dist/lib/watchdog/read.js +0 -1
  190. package/dist/lib/watchdog/rotate.js +1 -2
  191. package/dist/lib/watchdog/runner.js +2 -2
  192. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  193. package/dist/lib/watchdog/watchdogTail.js +0 -31
  194. package/package.json +1 -1
  195. package/scripts/postinstall.js +8 -8
  196. package/dist/browser.d.ts +0 -2
  197. package/dist/browser.js +0 -17
  198. package/dist/commands/browser-picker.d.ts +0 -18
  199. package/dist/commands/browser-picker.js +0 -97
  200. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  201. package/dist/lib/browser/arc-discovery.js +0 -197
  202. package/dist/lib/browser/arc-dom.d.ts +0 -14
  203. package/dist/lib/browser/arc-dom.js +0 -121
  204. package/dist/lib/browser/caller-identity.d.ts +0 -40
  205. package/dist/lib/browser/caller-identity.js +0 -175
  206. package/dist/lib/browser/cdp.d.ts +0 -51
  207. package/dist/lib/browser/cdp.js +0 -254
  208. package/dist/lib/browser/chrome.d.ts +0 -141
  209. package/dist/lib/browser/chrome.js +0 -704
  210. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  211. package/dist/lib/browser/chromium-discovery.js +0 -96
  212. package/dist/lib/browser/devices.d.ts +0 -34
  213. package/dist/lib/browser/devices.js +0 -61
  214. package/dist/lib/browser/domain-skills.d.ts +0 -71
  215. package/dist/lib/browser/domain-skills.js +0 -195
  216. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  217. package/dist/lib/browser/drivers/arc.js +0 -276
  218. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  219. package/dist/lib/browser/drivers/firefox.js +0 -377
  220. package/dist/lib/browser/drivers/local.d.ts +0 -55
  221. package/dist/lib/browser/drivers/local.js +0 -266
  222. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  223. package/dist/lib/browser/drivers/ssh.js +0 -467
  224. package/dist/lib/browser/editor.d.ts +0 -3
  225. package/dist/lib/browser/editor.js +0 -50
  226. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  227. package/dist/lib/browser/ffmpeg.js +0 -184
  228. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  229. package/dist/lib/browser/firefox-discovery.js +0 -162
  230. package/dist/lib/browser/har.d.ts +0 -85
  231. package/dist/lib/browser/har.js +0 -77
  232. package/dist/lib/browser/hygiene.d.ts +0 -97
  233. package/dist/lib/browser/hygiene.js +0 -153
  234. package/dist/lib/browser/index.d.ts +0 -5
  235. package/dist/lib/browser/index.js +0 -5
  236. package/dist/lib/browser/input.d.ts +0 -7
  237. package/dist/lib/browser/input.js +0 -92
  238. package/dist/lib/browser/ipc.d.ts +0 -204
  239. package/dist/lib/browser/ipc.js +0 -1453
  240. package/dist/lib/browser/login-detection.d.ts +0 -87
  241. package/dist/lib/browser/login-detection.js +0 -274
  242. package/dist/lib/browser/profiles.d.ts +0 -422
  243. package/dist/lib/browser/profiles.js +0 -1157
  244. package/dist/lib/browser/refs.d.ts +0 -89
  245. package/dist/lib/browser/refs.js +0 -191
  246. package/dist/lib/browser/registry.d.ts +0 -70
  247. package/dist/lib/browser/registry.js +0 -252
  248. package/dist/lib/browser/remote-control.d.ts +0 -59
  249. package/dist/lib/browser/remote-control.js +0 -85
  250. package/dist/lib/browser/resolve-target.d.ts +0 -66
  251. package/dist/lib/browser/resolve-target.js +0 -264
  252. package/dist/lib/browser/runtime-state.d.ts +0 -291
  253. package/dist/lib/browser/runtime-state.js +0 -584
  254. package/dist/lib/browser/secret-ref.d.ts +0 -10
  255. package/dist/lib/browser/secret-ref.js +0 -14
  256. package/dist/lib/browser/service.d.ts +0 -812
  257. package/dist/lib/browser/service.js +0 -4693
  258. package/dist/lib/browser/stream.d.ts +0 -17
  259. package/dist/lib/browser/stream.js +0 -72
  260. package/dist/lib/browser/task-index.d.ts +0 -78
  261. package/dist/lib/browser/task-index.js +0 -199
  262. package/dist/lib/browser/types.d.ts +0 -647
  263. package/dist/lib/browser/types.js +0 -61
  264. package/dist/lib/browser/upload.d.ts +0 -29
  265. package/dist/lib/browser/upload.js +0 -298
  266. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  267. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  268. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  269. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  270. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  271. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -23,12 +23,11 @@ import { detectOverdueJobs, notifyOverdue } from '../overdue.js';
23
23
  import { runCatchup } from '../catchup.js';
24
24
  import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } from '../routine-notify.js';
25
25
  import { notifyOwnerRoutineFinish, notifyOwnerRoutineStartFailed } from '../routine-notify-owner.js';
26
- import { getSocketPath as getBrowserIpcSocketPath } from '../browser/ipc.js';
27
26
  import { redactSecrets } from '../redact.js';
28
27
  import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from '../cli-entry.js';
29
28
  import { localBinDir } from '../platform/posixpath.js';
30
29
  import { isSchedulerEnabled, assertSchedulerEnabled, isDaemonEnabled } from '../device-config.js';
31
- import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, readSubsystemHealth, SUBSYSTEM_DAEMON_START } from '../daemon-health.js';
30
+ import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, readSubsystemHealth, readRecentDaemonRestarts, SUBSYSTEM_DAEMON_START } from '../daemon-health.js';
32
31
  import { ServiceSupervisor } from './supervisor.js';
33
32
  import { emit, emitAsync, emitRoutineEnd } from '../feed/events.js';
34
33
  import { readDaemonServicesConfig, drainDaemonServiceRestartQueue } from '../daemon-services.js';
@@ -40,8 +39,8 @@ import { sleepSync } from '../fs-atomic.js';
40
39
  * survive across processes goes through `daemon-health.ts` instead (which
41
40
  * `ServiceSupervisor` already writes on every tick). This getter exists so a
42
41
  * FUTURE same-process consumer (e.g. a `daemon services` live-status IPC
43
- * handler) can read the supervisor's richer state (`parked`, not just
44
- * ok/error) without needing its own reference to `runDaemon()`'s locals.
42
+ * handler) can read the supervisor's live state (`running`/`stopped`, plus the
43
+ * failure streak) without needing its own reference to `runDaemon()`'s locals.
45
44
  */
46
45
  let activeServiceSupervisor = null;
47
46
  /** Health for every service currently registered on the live supervisor, or `null` if the daemon isn't running in this process. */
@@ -127,31 +126,33 @@ export function daemonSystemdUnitName() {
127
126
  const WEDGE_THRESHOLD_TICKS = 3;
128
127
  const DAEMON_HEARTBEAT_TICK_MS = 60_000;
129
128
  /**
130
- * Crash-loop prevention (RUSH-2418). Three layers, because none of them alone
131
- * bounds a daemon that dies during startup:
129
+ * Crash-loop pacing and auto-start bounding (RUSH-2418, PHNX-4116). Two layers,
130
+ * because a daemon that dies during startup must be paced but must NEVER be
131
+ * abandoned:
132
132
  *
133
- * 1. **The OS supervisor paces the respawn.** `KeepAlive` with no
134
- * `ThrottleInterval` lets launchd relaunch on its ~10s default, so a daemon
135
- * that dies while booting is restarted six times a minute forever — the exact
136
- * failure the menu-bar helper hit (`menubar/install-menubar.ts`: 38 orphaned
137
- * `agents doctor` children, load average 490). systemd's `Restart=always`
138
- * with no `StartLimit*` is the same uncapped loop.
139
- * 2. **`StartLimitBurst` gives systemd a real cap** — after this many starts
140
- * inside the interval the unit is put in `failed` and stops respawning, so a
141
- * genuinely broken install stops burning the box and `systemctl --user status`
142
- * names it. launchd has no burst equivalent; the throttle is its whole answer.
143
- * 3. **The application-level circuit breaker** below stops *auto*-starts from
144
- * re-entering the loop from the other direction — a foreground command that
145
- * calls `ensureDaemonStarted()` on every invocation.
133
+ * 1. **The OS supervisor paces AND always retries the respawn.** launchd's
134
+ * `KeepAlive` with `ThrottleInterval=30` (and systemd's `Restart=always`
135
+ * `RestartSec=30`) relaunch a dead daemon every ~30s. Crucially systemd's
136
+ * `StartLimitIntervalSec=0` REMOVES the burst cap (PHNX-4116): a repeatedly
137
+ * deadline-breaching daemon is restarted every ~30s indefinitely rather than
138
+ * parked in `failed` and left dark until a human runs `systemctl restart`.
139
+ * The owner requirement is that a wedged daemon always recovers on its own —
140
+ * "just work like systemd/launchd". A supervised deadline breach is precisely
141
+ * what forces the exit (see `ServiceSupervisor.exitForRestart`), so the exit
142
+ * is the recovery, not a failure to give up on.
143
+ * 2. **The application-level circuit breaker** below still stops *auto*-starts
144
+ * from re-entering the loop from the other direction — a foreground command
145
+ * that calls `ensureDaemonStarted()` on every invocation. This bounds the
146
+ * IMPLICIT starts a busy operator would otherwise trigger; the OS-level
147
+ * unit-restart above is deliberately unbounded.
146
148
  */
147
149
  const DAEMON_THROTTLE_SECONDS = 30;
148
- const DAEMON_START_LIMIT_INTERVAL_SECONDS = 300;
149
- const DAEMON_START_LIMIT_BURST = 5;
150
150
  /**
151
151
  * How many consecutive failed daemon starts disable the *implicit* auto-start
152
- * (`ensureDaemonStarted`). Matches `DAEMON_START_LIMIT_BURST` so the two layers
153
- * give up together rather than one silently masking the other. `agents daemon
154
- * start` is the deliberate override and is never gated by this.
152
+ * (`ensureDaemonStarted`). This bounds foreground-command auto-starts only; the
153
+ * OS-level unit restart is deliberately unbounded (`StartLimitIntervalSec=0`) so
154
+ * a wedged daemon always recovers (PHNX-4116). `agents daemon start` is the
155
+ * deliberate override and is never gated by this.
155
156
  */
156
157
  export const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
157
158
  export function schedulerGateTransition(running, enabled) {
@@ -338,28 +339,19 @@ export function removeHeartbeat() {
338
339
  catch { /* already removed */ }
339
340
  }
340
341
  /**
341
- * A heartbeat is "fresh" when its last tick falls inside the wedge window — the
342
- * same threshold isDaemonWedged() uses to decide a still-present daemon has gone
343
- * unresponsive. A fresh heartbeat whose pid is alive is proof of a live, ticking
344
- * daemon even when the pid file has been lost.
342
+ * A heartbeat is "fresh" when its last tick falls inside the freshness window. A
343
+ * fresh heartbeat whose pid is alive is proof of a live, ticking daemon even
344
+ * when the pid file has been lost, which is why `resolveLiveDaemonPid` trusts it.
345
+ *
346
+ * There is no separate "wedged" verdict any more (PHNX-4116): a daemon whose
347
+ * event loop stalls no longer sits `wedged` waiting for a human — a supervised
348
+ * service that breaches its deadline exits the process, and systemd/launchd
349
+ * restart it. So the daemon is simply `running` or `stopped`.
345
350
  */
346
351
  function isHeartbeatFresh(hb) {
347
352
  const elapsed = Date.now() - Date.parse(hb.lastTick);
348
353
  return elapsed <= WEDGE_THRESHOLD_TICKS * DAEMON_HEARTBEAT_TICK_MS;
349
354
  }
350
- export function isDaemonWedged() {
351
- const pid = readDaemonPid();
352
- if (!pid)
353
- return false;
354
- if (!isLiveDaemon(pid))
355
- return false;
356
- const hb = readHeartbeat();
357
- if (!hb)
358
- return false;
359
- if (hb.pid !== pid)
360
- return false;
361
- return !isHeartbeatFresh(hb);
362
- }
363
355
  /** How long stopDaemon waits for a SIGTERMed daemon to exit before escalating. */
364
356
  const STOP_GRACE_MS = 5000;
365
357
  /** How long it waits after the hard tree-kill before giving up. */
@@ -488,10 +480,11 @@ export function claimDaemonInstance() {
488
480
  const existing = resolveLiveDaemonPid(true);
489
481
  if (existing !== null && existing !== process.pid) {
490
482
  // Evict, and WAIT for the incumbent to be provably dead — its graceful
491
- // handleShutdown releasing the browser IPC binding — before we write our
492
- // pid and (later, in runDaemon) bind our own. Binding before the release
493
- // recreates the two-owners-on-one-socket orphan documented at stopDaemon
494
- // below, so the pid file is not written until the prior owner is gone.
483
+ // handleShutdown releasing its socket bindings (feed-stream hub, monitor)
484
+ // — before we write our pid and (later, in runDaemon) bind our own.
485
+ // Binding before the release recreates the two-owners-on-one-socket orphan
486
+ // documented at stopDaemon below, so the pid file is not written until the
487
+ // prior owner is gone.
495
488
  if (!evictIncumbentDaemon(existing))
496
489
  return false;
497
490
  }
@@ -504,10 +497,11 @@ export function claimDaemonInstance() {
504
497
  }
505
498
  /**
506
499
  * SIGTERM a live incumbent daemon and block until it is provably dead, so its
507
- * graceful handleShutdown has released the browser IPC binding BEFORE the
508
- * newcomer binds anything of its own (SING-11) — the daemon no longer hosts a
509
- * secrets broker socket to release (the standalone `secrets` CLI owns it now,
510
- * PHNX-3989 OWN-1). Escalates
500
+ * graceful handleShutdown has released its socket bindings (the feed-stream hub,
501
+ * the monitor socket) BEFORE the newcomer binds anything of its own (SING-11) —
502
+ * the daemon no longer hosts a secrets broker socket (the standalone `secrets`
503
+ * CLI owns it, PHNX-3989 OWN-1) nor a browser IPC socket (the standalone
504
+ * `browser` CLI owns it, PHNX-4101). Escalates
511
505
  * to killTree after the grace window. Passes the POSITIVE pid so the kill reaches
512
506
  * only the incumbent daemon — never its detached routine children, which run in
513
507
  * their own process groups and must survive takeover (SING-11a); the new daemon
@@ -878,15 +872,13 @@ export async function runDaemon() {
878
872
  assertTestDaemonHome();
879
873
  // Lifecycle readers and launchers do not run services. Load their code only
880
874
  // in the daemon process, before it claims or publishes lifecycle state.
881
- const [{ BrowserService }, { SessionIndexService }, { SessionSummarizerService }, { SessionTitleService }, { MonitorEngineService }, { AccountUsageService, AccountAuthService }, { CatchupService }, { BrowserIPCService }, { WatchdogService }, { DeviceProbeService }, { SelfHealService }, { SelfUpdateService }, { HarnessUpdateService }, { AuthSyncService }, { UsageSyncService }, { StateDirCheckService }, { SessionStateService }, { FeedStreamService }, { AttentionNotifyService }, { WebhookReceiverService }, { HeartbeatService }, { TmuxReapService }, { BrowserTaskReapService },] = await Promise.all([
882
- import('../browser/service.js'),
875
+ const [{ SessionIndexService }, { SessionSummarizerService }, { SessionTitleService }, { MonitorEngineService }, { AccountUsageService, AccountAuthService }, { CatchupService }, { WatchdogService }, { DeviceProbeService }, { SelfHealService }, { SelfUpdateService }, { HarnessUpdateService }, { AuthSyncService }, { UsageSyncService }, { StateDirCheckService }, { SessionStateService }, { FeedStreamService }, { AttentionNotifyService }, { WebhookReceiverService }, { HeartbeatService }, { TmuxReapService },] = await Promise.all([
883
876
  import('./session-index-service.js'),
884
877
  import('./session-summarizer-service.js'),
885
878
  import('./session-title-service.js'),
886
879
  import('./monitor-engine-service.js'),
887
880
  import('./account-state-daemon-service.js'),
888
881
  import('./catchup-service.js'),
889
- import('./browser-ipc-service.js'),
890
882
  import('./watchdog-service.js'),
891
883
  import('./device-probe-service.js'),
892
884
  import('./self-heal-service.js'),
@@ -901,7 +893,6 @@ export async function runDaemon() {
901
893
  import('./webhook-receiver-service.js'),
902
894
  import('./heartbeat-service.js'),
903
895
  import('./tmux-reap-service.js'),
904
- import('./browser-task-reap-service.js'),
905
896
  ]);
906
897
  // Install the shared-daemon reload signal boundary BEFORE publishing our PID
907
898
  // in claimDaemonInstance(). Browser/routines clients use that PID to decide a
@@ -1011,11 +1002,11 @@ export async function runDaemon() {
1011
1002
  catch (err) {
1012
1003
  log('ERROR', `Stray daemon reaper failed: ${err.message}`);
1013
1004
  }
1014
- // Socket services: monitor engine, account-state, and browser IPC are all
1015
- // managed by the ServiceSupervisor (RUSH-3193 P2). The secrets broker moved
1016
- // with the standalone `secrets` engine (PHNX-3989 OWN-1) — this daemon no
1017
- // longer hosts or takes over that broker; the standalone owns its own
1018
- // lifecycle exclusively.
1005
+ // Socket services: monitor engine, account-state, and the feed-stream hub are
1006
+ // all managed by the ServiceSupervisor (RUSH-3193 P2). The secrets broker moved
1007
+ // with the standalone `secrets` engine (PHNX-3989 OWN-1) and the browser IPC
1008
+ // service with the standalone `browser` CLI (PHNX-4101) — this daemon no longer
1009
+ // hosts either; each standalone owns its own lifecycle exclusively.
1019
1010
  const supervisor = new ServiceSupervisor();
1020
1011
  if (isEnabled('session-state')) {
1021
1012
  supervisor.register(new SessionStateService(() => supervisor.runNow('session-state')));
@@ -1023,8 +1014,7 @@ export async function runDaemon() {
1023
1014
  else
1024
1015
  log('INFO', 'Live session-state service disabled');
1025
1016
  // The shared feed fan-out. Registered even when disabled at boot so a later
1026
- // `agents daemon services enable feed-stream` brings it up over SIGHUP, the
1027
- // same live-transition shape browser IPC uses.
1017
+ // `agents daemon services enable feed-stream` brings it up over SIGHUP.
1028
1018
  supervisor.register(new FeedStreamService(), { enabled: isEnabled('feed-stream') });
1029
1019
  if (!isEnabled('feed-stream'))
1030
1020
  log('INFO', 'Shared feed stream service disabled');
@@ -1034,8 +1024,8 @@ export async function runDaemon() {
1034
1024
  else
1035
1025
  log('INFO', 'Monitor engine disabled');
1036
1026
  // Usage and auth refresh are two INDEPENDENT supervised services (PHNX-3608)
1037
- // so a run of usage-refresh failures parks only usage and never starves the
1038
- // slower auth refresh — each carries its own circuit breaker.
1027
+ // so a run of usage-refresh throws is recorded against usage alone and never
1028
+ // starves the slower auth refresh — each keeps ticking on its own interval.
1039
1029
  if (isEnabled('account-state'))
1040
1030
  supervisor.register(new AccountUsageService());
1041
1031
  else
@@ -1066,14 +1056,11 @@ export async function runDaemon() {
1066
1056
  else {
1067
1057
  log('INFO', 'Catch-up recovery service disabled');
1068
1058
  }
1069
- // BrowserIPCService and BrowserTaskReapService share one long-lived
1070
- // BrowserService. Browser IPC is registered even when disabled at boot so an
1071
- // explicit later `agents browser start` can enable it live over SIGHUP — the
1072
- // client owns a service transition, never a whole-daemon restart (PHNX-3605).
1073
- const browserService = new BrowserService();
1074
- supervisor.register(new BrowserIPCService(browserService), { enabled: isEnabled('browser-ipc') });
1075
- if (!isEnabled('browser-ipc'))
1076
- log('INFO', 'Browser IPC service disabled');
1059
+ // The browser IPC service and its task reaper are the standalone `browser`
1060
+ // CLI's now (@phnx-labs/browser-cli, PHNX-4101): it hosts its own IPC service
1061
+ // and runs its own `prune` reaper. agents-cli no longer constructs a
1062
+ // BrowserService, binds a browser socket, or reaps browser tasks — see
1063
+ // `commands/browser.ts` and `docs/browser.md`.
1077
1064
  if (isEnabled('session-index'))
1078
1065
  supervisor.register(new SessionIndexService());
1079
1066
  else
@@ -1143,17 +1130,11 @@ export async function runDaemon() {
1143
1130
  supervisor.register(new TmuxReapService());
1144
1131
  else
1145
1132
  log('INFO', 'Tmux reap service disabled');
1146
- if (isEnabled('browser-task-reap')) {
1147
- supervisor.register(new BrowserTaskReapService(browserService));
1148
- }
1149
- else {
1150
- log('INFO', 'Browser-task reap service disabled');
1151
- }
1152
1133
  await supervisor.startAll({ log });
1153
1134
  activeServiceSupervisor = supervisor;
1154
1135
  // scheduler.enabled=false in this machine's device doc means NO routines fire
1155
1136
  // here — the scheduler and its catchup recovery simply never start, while the
1156
- // daemon keeps its other duties (browser IPC, session sync).
1137
+ // daemon keeps its other duties (feed stream, session sync).
1157
1138
  // The refusal message is the same one the start surfaces
1158
1139
  // (`routines add` auto-start, manual `routines start`) raise. The gate is
1159
1140
  // re-evaluated on every SIGHUP reload (handleReload below) via
@@ -1353,14 +1334,14 @@ export async function runDaemon() {
1353
1334
  }
1354
1335
  catch (err) {
1355
1336
  // Ordinary pass errors are logged, not re-thrown: a transient catchup
1356
- // failure should not trip the circuit breaker. A HANG is still caught — the
1357
- // CatchupService deadline aborts the tick and the supervisor parks +
1358
- // restarts it regardless of this swallow (PHNX-3608).
1337
+ // failure should not be counted as a service failure. A HANG is still
1338
+ // caught — the CatchupService deadline aborts the tick and the supervisor
1339
+ // exits the daemon for an OS restart regardless of this swallow (PHNX-4116).
1359
1340
  log('ERROR', `Catchup pass failed: ${err.message}`);
1360
1341
  }
1361
1342
  }
1362
- // Browser orphan reap and IPC server start are now managed by BrowserIPCService
1363
- // on the supervisor (RUSH-3193 P2). The orphan reap runs inside onStart().
1343
+ // The browser IPC server and its orphan reap left this daemon with the
1344
+ // standalone `browser` CLI (PHNX-4101) — it hosts its own IPC service now.
1364
1345
  // Webhook receivers: signed webhook receiver(s) + their funnel (RUSH-2548).
1365
1346
  // Resolves each receiver's signing secret headlessly through the standalone
1366
1347
  // `secrets` CLI (an agentOnly secrets-client read) — no AGENTS_SECRETS_PASSPHRASE,
@@ -1379,15 +1360,13 @@ export async function runDaemon() {
1379
1360
  // RUSH-2501: reap tmux sessions whose panes are all dead. Daemon-only
1380
1361
  // (single executor). Dead managed panes and their orphan helpers are
1381
1362
  // reaped by TmuxReapService.
1382
- // RUSH-2622: close abandoned browser-task tabs on the same 5-min cadence,
1383
- // reusing the daemon's long-lived BrowserService when browser IPC is enabled.
1384
- // Abandoned browser tasks are reaped by BrowserTaskReapService, sharing the
1385
- // same BrowserService instance as BrowserIPCService.
1363
+ // Abandoned browser-task reaping (RUSH-2622) left this daemon with the
1364
+ // standalone `browser` CLI (PHNX-4101) — its own `prune` reaper owns it now.
1386
1365
  // RUSH-2367 / RUSH-3193 P3: state-dir-check (self-terminate guard) is
1387
1366
  // registered on the supervisor further below, once `handleShutdown` exists
1388
1367
  // — see the registration site after its declaration for why.
1389
- // RUSH-2418: startup is over — the scheduler, browser IPC, broker decision,
1390
- // monitor engine and every background tick are up. Only NOW does this daemon
1368
+ // RUSH-2418: startup is over — the scheduler, feed stream, monitor engine and
1369
+ // every background tick are up. Only NOW does this daemon
1391
1370
  // clear the auto-start failure streak `ensureDaemonStarted` reads. Clearing it
1392
1371
  // at claim time instead would reset the breaker for a process that dies while
1393
1372
  // initializing a subsystem, which is exactly the crash loop it exists to stop.
@@ -1670,14 +1649,14 @@ export function generateSystemdUnit(agentsBin = getAgentsBinPath()) {
1670
1649
  return `[Unit]
1671
1650
  Description=Agents Daemon - Scheduled Job Runner
1672
1651
  After=network.target
1673
- StartLimitIntervalSec=${DAEMON_START_LIMIT_INTERVAL_SECONDS}
1674
- StartLimitBurst=${DAEMON_START_LIMIT_BURST}
1652
+ StartLimitIntervalSec=0
1675
1653
 
1676
1654
  [Service]
1677
1655
  Type=simple
1678
1656
  ExecStart=${execStart}
1679
1657
  Restart=always
1680
1658
  RestartSec=${DAEMON_THROTTLE_SECONDS}
1659
+ KillMode=process
1681
1660
  Environment=PATH=${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin'])}
1682
1661
  Environment=HOME=${home}
1683
1662
  Environment=AGENTS_REAL_HOME=${realHome}
@@ -1989,7 +1968,7 @@ function startDaemonLocked(agentsBin, releaseLock) {
1989
1968
  * console-owning wrapper (cmd.exe / the npm shim). When that wrapper exits it
1990
1969
  * closes its console, and the detached daemon sharing that console receives a
1991
1970
  * console-close event that trips its shutdown handler — the daemon comes up,
1992
- * binds the browser IPC socket, then tears itself down ~36ms later (#556).
1971
+ * binds its sockets, then tears itself down ~36ms later (#556).
1993
1972
  * Going through `process.execPath` means a real PE/binary is spawned with
1994
1973
  * `detached: true` and no console, so nothing signals the daemon after launch.
1995
1974
  *
@@ -2279,13 +2258,11 @@ export function findSurvivingStateDirDaemons(exclude) {
2279
2258
  * from launchd/systemd if applicable.
2280
2259
  *
2281
2260
  * The SIGTERM → grace → killTree sequence is unchanged; what it adds is
2282
- * verification. After the daemon is gone it checks that the browser IPC
2283
- * binding actually released — a stale socket present on disk but with no live
2284
- * owner is the orphan that keeps clients hanging on it — and that no
2285
- * `__daemon-run` for THIS state dir survives. A killTree escalation exits
2286
- * without running the daemon's graceful handleShutdown, so that socket can be
2287
- * left stale; this reclaims it (the owner is provably dead) and reports it. It
2288
- * never reports success on an unverified stop.
2261
+ * verification: after the daemon is gone it checks that no `__daemon-run` for
2262
+ * THIS state dir survives and reclaims the state files a killTree escalation
2263
+ * (which skips the graceful handleShutdown) can leave stale. The browser IPC
2264
+ * socket is no longer among them — it left with the standalone `browser` CLI
2265
+ * (PHNX-4101). It never reports success on an unverified stop.
2289
2266
  */
2290
2267
  export function stopDaemon() {
2291
2268
  const releaseLock = acquireLifecycleLock();
@@ -2341,16 +2318,6 @@ function stopDaemonLocked() {
2341
2318
  // excluded. resolveLiveDaemonPid(true) rejects a reused/non-daemon pid before
2342
2319
  // any service-manager teardown or direct signal and repairs only under lock.
2343
2320
  const pid = resolveLiveDaemonPid(true);
2344
- const browserSock = process.platform === 'win32' ? null : getBrowserIpcSocketPath();
2345
- // Capture the socket's inode INDEPENDENTLY of whether a live pid resolved
2346
- // (PHNX-3618). A daemon that died between the CLI liveness precheck and this
2347
- // locked read leaves resolveLiveDaemonPid() returning null while its ungraceful
2348
- // exit left the binding on disk — capturing only when `pid !== null` meant that
2349
- // genuinely stale socket could never be reclaimed and was reported "ownership
2350
- // could not be verified". The inode is the successor guard: a fresh daemon that
2351
- // rebinds during the stop gets a new inode, so a later identity match still
2352
- // proves this exact binding is the one we captured, never a successor's.
2353
- const browserSockOwner = browserSock ? readPathIdentity(browserSock) : null;
2354
2321
  if (platform === 'darwin') {
2355
2322
  const plistPath = getLaunchdPlistPath();
2356
2323
  if (fs.existsSync(plistPath)) {
@@ -2476,49 +2443,9 @@ function stopDaemonLocked() {
2476
2443
  else if (pid === null && currentPid !== null) {
2477
2444
  surviving.push(`daemon pid registration changed to ${currentPid} during stop`);
2478
2445
  }
2479
- // Browser IPC binding: on POSIX the listening socket is a filesystem object.
2480
- // A graceful handleShutdown unlinks it; if it survives, the daemon exited
2481
- // ungracefully (killTree) and left a stale binding — the owner is provably
2482
- // dead, so reclaim it and report.
2483
- if (process.platform !== 'win32') {
2484
- // A binding is provably stale — and reclaimable — when the inode on disk is
2485
- // still the exact one captured under this lock AND no daemon (the signalled
2486
- // target OR any successor for this state dir) survives to own it. Keying the
2487
- // "a daemon still owns it" test on the whole survivor set rather than only the
2488
- // captured target is what lets a daemon that died BEFORE this stop (pid ===
2489
- // null, PHNX-3618) still have its stale socket reclaimed, while a live
2490
- // successor — which either rebound the inode or shows up as a survivor — is
2491
- // never touched (PHNX-3607's never-delete-a-successor invariant).
2492
- const aDaemonSurvives = survivors.length > 0 || unverifiedSurvivors.length > 0;
2493
- if (browserSock && fs.existsSync(browserSock)) {
2494
- const ownsCapturedBinding = browserSockOwner !== null
2495
- && pathIdentityMatches(browserSock, browserSockOwner);
2496
- if (ownsCapturedBinding && !aDaemonSurvives) {
2497
- try {
2498
- fs.unlinkSync(browserSock);
2499
- }
2500
- catch { /* failed to reclaim the captured binding */ }
2501
- if (pathIdentityMatches(browserSock, browserSockOwner))
2502
- surviving.push('browser IPC socket not released');
2503
- else if (fs.existsSync(browserSock))
2504
- surviving.push('browser IPC socket ownership changed during stop');
2505
- else
2506
- released.push('browser IPC socket (reclaimed)');
2507
- }
2508
- else if (aDaemonSurvives) {
2509
- surviving.push('browser IPC socket still owned by a surviving daemon');
2510
- }
2511
- else {
2512
- // The path was absent when this transaction captured the target (a
2513
- // successor bound it afterward) or a successor replaced the inode. Either
2514
- // way this invocation does not own the current binding and must leave it.
2515
- surviving.push('browser IPC socket ownership could not be verified');
2516
- }
2517
- }
2518
- else {
2519
- released.push('browser IPC socket');
2520
- }
2521
- }
2446
+ // The browser IPC socket is the standalone `browser` CLI's now (PHNX-4101):
2447
+ // the daemon no longer binds it, so there is nothing to reclaim here. browser-cli
2448
+ // owns its own socket lifecycle under `~/.agents/.cache/helpers/browser/`.
2522
2449
  // ── The state files a killed daemon cannot clean up itself (RUSH-2421) ─────
2523
2450
  // handleShutdown removes the lifetime marker, the heartbeat and this pid's
2524
2451
  // instance-registry entry — but it only runs on the GRACEFUL path. Every
@@ -2556,10 +2483,16 @@ function stopDaemonLocked() {
2556
2483
  detachedChildren,
2557
2484
  };
2558
2485
  }
2559
- /** Get current daemon status including running state, PID, and enabled job count. */
2486
+ /**
2487
+ * Get current daemon status including running state, PID, enabled job count, and
2488
+ * the supervised-restart history `agents daemon status` renders (PHNX-4116).
2489
+ *
2490
+ * There is no `wedged` state: a stalled daemon exits for a supervised
2491
+ * systemd/launchd restart rather than sitting unresponsive, so the daemon is
2492
+ * simply `running` or `stopped`, and its recent restarts are reported instead.
2493
+ */
2560
2494
  export function getDaemonStatus() {
2561
2495
  const running = isDaemonRunning();
2562
- const wedged = running && isDaemonWedged();
2563
2496
  const pid = readDaemonPid();
2564
2497
  let jobCount = 0;
2565
2498
  try {
@@ -2571,14 +2504,19 @@ export function getDaemonStatus() {
2571
2504
  binaryPath = getAgentsBinPath();
2572
2505
  }
2573
2506
  catch { /* resolution failed */ }
2507
+ const recentRestarts = readRecentDaemonRestarts(Date.now() - 24 * 60 * 60 * 1000);
2508
+ const lastRestart = recentRestarts.length > 0 ? recentRestarts[recentRestarts.length - 1] : null;
2574
2509
  return {
2575
- state: wedged ? 'wedged' : running ? 'running' : 'stopped',
2510
+ state: running ? 'running' : 'stopped',
2576
2511
  running,
2577
2512
  pid,
2578
2513
  jobCount,
2579
2514
  logPath: getDaemonLogPath(),
2580
2515
  binaryPath,
2581
2516
  heartbeat: readHeartbeat(),
2517
+ restarts24h: recentRestarts.length,
2518
+ lastRestartCause: lastRestart ? `${lastRestart.subsystem}: ${lastRestart.cause}` : null,
2519
+ lastRestartAt: lastRestart ? lastRestart.at : null,
2582
2520
  };
2583
2521
  }
2584
2522
  /** Read the daemon log, optionally limited to the last N lines. */
@@ -87,8 +87,8 @@ export declare function routineSpawnCwd(config: Pick<JobConfig, 'name' | 'projec
87
87
  * modeFlags.plan so claudeAdapter.routineModeArgs can splice
88
88
  * plan → acceptEdits / auto / skip.
89
89
  *
90
- * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
91
- * and every other harness the daemon does not fire locally).
90
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (grok and every
91
+ * other harness the daemon does not fire locally).
92
92
  */
93
93
  export declare function bakeRoutineArgv(agent: string): string[] | undefined;
94
94
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
@@ -43,7 +43,25 @@ import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountCandidate, r
43
43
  import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
44
44
  import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
45
45
  import { isCustomHarnessName, readProfile } from '../profiles.js';
46
- import { findAccount, findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } from '../account-registry.js';
46
+ import { findAccount, findUnifiedAccount, listNativeAccounts, resolveAccountSelection, resolveCredentialAccount } from '../account-registry.js';
47
+ import { recordRunAuthOutcome } from '../auth-health.js';
48
+ /**
49
+ * Record a routine run's auth outcome as a per-account FACT (PHNX-4116). A worker
50
+ * runs entirely through routines and never probes, so an auth failure here — or a
51
+ * clean success that clears a stale failure — is the honest evidence `agents view`
52
+ * renders as `last used ok` / `last auth failure`. Best-effort: resolves the slot
53
+ * from the routine's account name so the fact lands on the right row, and never
54
+ * throws (a run it cannot attribute is simply not recorded).
55
+ */
56
+ function recordRoutineAuthOutcome(agent, accountName, version, outcome) {
57
+ try {
58
+ const accountId = accountName
59
+ ? listNativeAccounts(readMeta()).find((a) => a.agent === agent && a.name === accountName)?.id ?? null
60
+ : null;
61
+ recordRunAuthOutcome({ agent, accountId, version: version ?? null, account: accountName, outcome });
62
+ }
63
+ catch { /* best-effort */ }
64
+ }
47
65
  export class RoutineAlreadyRunningError extends Error {
48
66
  constructor(jobName, runId) {
49
67
  super(`Routine '${jobName}' already has a running execution (${runId})`);
@@ -465,7 +483,6 @@ const ROUTINE_TRANSCRIPT_SPECS = {
465
483
  claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
466
484
  codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
467
485
  cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
468
- gemini: [{ root: ['.gemini', 'tmp'], ext: '.json' }],
469
486
  antigravity: [{ root: ['.gemini', 'antigravity-cli', 'conversations'], ext: '.db' }],
470
487
  droid: [{ root: ['.factory', 'sessions'], ext: '.jsonl' }],
471
488
  // Kimi splits a session across two files (session/discover.ts:4382-4384):
@@ -506,8 +523,8 @@ export function routineSpawnCwd(config) {
506
523
  * modeFlags.plan so claudeAdapter.routineModeArgs can splice
507
524
  * plan → acceptEdits / auto / skip.
508
525
  *
509
- * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
510
- * and every other harness the daemon does not fire locally).
526
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (grok and every
527
+ * other harness the daemon does not fire locally).
511
528
  */
512
529
  export function bakeRoutineArgv(agent) {
513
530
  if (!ROUTINE_AGENT_IDS.includes(agent))
@@ -1577,6 +1594,7 @@ async function executeJobPlaced(config, deps, attempt) {
1577
1594
  // success (processFailed:false).
1578
1595
  if (isAuthFailureFromLog(attempt.logText, effectiveAgent, { processFailed: false })) {
1579
1596
  const reason = authFailureReason(attempt.logText) ?? 'authentication_failed';
1597
+ recordRoutineAuthOutcome(attemptAgent, config.account, attemptVersion, { ok: false, verdict: 'revoked', detail: reason });
1580
1598
  finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, { errorMessage: `auth_failed: ${reason}` });
1581
1599
  writeRunMeta(meta);
1582
1600
  timer.end({ status: 'failed', exitCode: meta.exitCode ?? undefined, runId, error: `auth_failed: ${reason}` });
@@ -1584,6 +1602,9 @@ async function executeJobPlaced(config, deps, attempt) {
1584
1602
  archiveRoutineTranscripts(meta, runDir, overlayHome);
1585
1603
  return { meta, reportPath: null };
1586
1604
  }
1605
+ // A clean routine completion is real auth evidence — record it so a worker's
1606
+ // token reads `last used ok` and a stale failure fact is cleared (PHNX-4116).
1607
+ recordRoutineAuthOutcome(attemptAgent, config.account, attemptVersion, { ok: true });
1587
1608
  finalizeRunMeta(meta, 'completed', 0);
1588
1609
  writeRunMeta(meta);
1589
1610
  timer.end({ status: 'completed', exitCode: 0, runId });
@@ -1619,6 +1640,8 @@ async function executeJobPlaced(config, deps, attempt) {
1619
1640
  const failureErrorMessage = authReason
1620
1641
  ? `auth_failed: ${authReason}`
1621
1642
  : (attempt.error ?? undefined);
1643
+ if (authFailed)
1644
+ recordRoutineAuthOutcome(attemptAgent, config.account, attemptVersion, { ok: false, verdict: 'revoked', detail: authReason ?? undefined });
1622
1645
  finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, failureErrorMessage ? { errorMessage: failureErrorMessage } : undefined);
1623
1646
  writeRunMeta(meta);
1624
1647
  timer.end({
@@ -2044,10 +2067,15 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
2044
2067
  // marker still catches an exit-0 auth failure on its own.
2045
2068
  if (isAuthFailureFromLog(logText, effectiveAgent, { processFailed: (code ?? 1) !== 0 })) {
2046
2069
  const reason = authFailureReason(logText) ?? 'authentication_failed';
2070
+ recordRoutineAuthOutcome(effectiveAgent, config.account, meta.version, { ok: false, verdict: 'revoked', detail: reason });
2047
2071
  settle('failed', code ?? 1, `auth_failed: ${reason}`);
2048
2072
  return;
2049
2073
  }
2050
2074
  const inferred = inferFinalStatusFromLog(stdoutPath, effectiveAgent);
2075
+ const finalStatus = inferred ? inferred.status : (code === 0 ? 'completed' : 'failed');
2076
+ // A clean completion clears any stale auth-failure fact for this account.
2077
+ if (finalStatus === 'completed')
2078
+ recordRoutineAuthOutcome(effectiveAgent, config.account, meta.version, { ok: true });
2051
2079
  if (inferred) {
2052
2080
  settle(inferred.status, inferred.exitCode);
2053
2081
  }
@@ -2201,11 +2229,6 @@ export function extractReport(stdoutPath, agentType) {
2201
2229
  : JSON.stringify(parsed.content);
2202
2230
  }
2203
2231
  }
2204
- if (agentType === 'gemini') {
2205
- if (parsed.type === 'text' && parsed.text) {
2206
- lastMessage = parsed.text;
2207
- }
2208
- }
2209
2232
  }
2210
2233
  catch { /* malformed JSONL line */ }
2211
2234
  }