@phnx-labs/agents-cli 1.22.117 → 1.22.118

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 (248) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.js +53 -1
  9. package/dist/commands/repo.js +13 -49
  10. package/dist/commands/route.js +5 -5
  11. package/dist/commands/routines.js +39 -12
  12. package/dist/commands/sessions-render.d.ts +1 -1
  13. package/dist/commands/sessions-render.js +0 -1
  14. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  15. package/dist/commands/sessions.test-fixture.js +0 -37
  16. package/dist/commands/setup-browser.d.ts +17 -12
  17. package/dist/commands/setup-browser.js +52 -87
  18. package/dist/commands/setup-preferences.d.ts +0 -17
  19. package/dist/commands/setup-preferences.js +18 -49
  20. package/dist/commands/setup.js +9 -9
  21. package/dist/commands/ssh.js +10 -1
  22. package/dist/commands/sync.js +6 -44
  23. package/dist/commands/teams.js +0 -1
  24. package/dist/commands/update.d.ts +19 -0
  25. package/dist/commands/update.js +34 -6
  26. package/dist/commands/view.d.ts +12 -1
  27. package/dist/commands/view.js +47 -2
  28. package/dist/index.js +107 -26
  29. package/dist/lib/account-capabilities.js +0 -2
  30. package/dist/lib/account-catalog.d.ts +38 -0
  31. package/dist/lib/account-catalog.js +110 -26
  32. package/dist/lib/account-provider-registry.js +1 -1
  33. package/dist/lib/account-registry.d.ts +1 -1
  34. package/dist/lib/account-registry.js +36 -2
  35. package/dist/lib/accounting/rotate.js +22 -8
  36. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  37. package/dist/lib/accounting/usage-ingest.js +115 -45
  38. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  39. package/dist/lib/accounting/usage-sync.js +227 -66
  40. package/dist/lib/accounting/usage.d.ts +16 -3
  41. package/dist/lib/accounting/usage.js +26 -8
  42. package/dist/lib/accounts/slots.d.ts +8 -0
  43. package/dist/lib/accounts/slots.js +17 -0
  44. package/dist/lib/add-dir.js +0 -1
  45. package/dist/lib/agent-cli-commands.js +0 -1
  46. package/dist/lib/agent-spec/agents.d.ts +6 -7
  47. package/dist/lib/agent-spec/agents.js +6 -60
  48. package/dist/lib/auth-health.d.ts +69 -1
  49. package/dist/lib/auth-health.js +133 -4
  50. package/dist/lib/browser/context.d.ts +82 -0
  51. package/dist/lib/browser/context.js +88 -0
  52. package/dist/lib/browser/paths.d.ts +25 -0
  53. package/dist/lib/browser/paths.js +51 -0
  54. package/dist/lib/browser/record.d.ts +38 -0
  55. package/dist/lib/browser/record.js +57 -0
  56. package/dist/lib/browser/sessions-list.js +1 -2
  57. package/dist/lib/browser-client.d.ts +158 -0
  58. package/dist/lib/browser-client.js +234 -0
  59. package/dist/lib/channels/owner-sink.d.ts +1 -18
  60. package/dist/lib/channels/owner-sink.js +29 -82
  61. package/dist/lib/channels/providers/rush.d.ts +7 -7
  62. package/dist/lib/channels/providers/rush.js +127 -57
  63. package/dist/lib/claude-statusline.d.ts +1 -1
  64. package/dist/lib/claude-statusline.js +15 -3
  65. package/dist/lib/commands.js +1 -1
  66. package/dist/lib/crabbox/lease.js +1 -1
  67. package/dist/lib/crabbox/runtimes.js +1 -3
  68. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  69. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  70. package/dist/lib/daemon/auth-sync-service.js +37 -43
  71. package/dist/lib/daemon/daemon.d.ts +25 -14
  72. package/dist/lib/daemon/daemon.js +92 -154
  73. package/dist/lib/daemon/runner.d.ts +2 -2
  74. package/dist/lib/daemon/runner.js +32 -9
  75. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  76. package/dist/lib/daemon/self-update-service.js +40 -36
  77. package/dist/lib/daemon/service.d.ts +12 -8
  78. package/dist/lib/daemon/supervisor.d.ts +65 -54
  79. package/dist/lib/daemon/supervisor.js +136 -171
  80. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  81. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  82. package/dist/lib/daemon/usage-sync-service.js +46 -78
  83. package/dist/lib/daemon-health.d.ts +22 -5
  84. package/dist/lib/daemon-health.js +47 -3
  85. package/dist/lib/daemon-services.d.ts +1 -1
  86. package/dist/lib/daemon-services.js +0 -10
  87. package/dist/lib/daemon-ticks.js +4 -2
  88. package/dist/lib/devices/doctor-findings.js +8 -11
  89. package/dist/lib/devices/harness-inventory.js +14 -5
  90. package/dist/lib/exec-account-home.js +5 -0
  91. package/dist/lib/exec.js +0 -11
  92. package/dist/lib/feed/events.js +0 -1
  93. package/dist/lib/feed/tool-activity.js +5 -2
  94. package/dist/lib/fleet-shared-state.d.ts +45 -0
  95. package/dist/lib/fleet-shared-state.js +78 -9
  96. package/dist/lib/fs-atomic.js +25 -0
  97. package/dist/lib/harness-auth-capabilities.js +0 -1
  98. package/dist/lib/helper-versions.js +1 -1
  99. package/dist/lib/hooks/cache.d.ts +7 -0
  100. package/dist/lib/hooks/cache.js +27 -2
  101. package/dist/lib/hooks/install.js +7 -1
  102. package/dist/lib/hosts/ready.d.ts +38 -8
  103. package/dist/lib/hosts/ready.js +62 -14
  104. package/dist/lib/installations/active-check.d.ts +27 -0
  105. package/dist/lib/installations/active-check.js +61 -0
  106. package/dist/lib/installations/migrate.js +6 -3
  107. package/dist/lib/installations/shims.js +9 -7
  108. package/dist/lib/installations/update.js +21 -15
  109. package/dist/lib/installations/versions.js +12 -1
  110. package/dist/lib/manifest.js +1 -1
  111. package/dist/lib/models.js +0 -2
  112. package/dist/lib/open-url.js +28 -54
  113. package/dist/lib/profiles.js +0 -2
  114. package/dist/lib/refresh.js +1 -1
  115. package/dist/lib/routine-readiness.d.ts +35 -0
  116. package/dist/lib/routine-readiness.js +74 -16
  117. package/dist/lib/rules/compile.js +6 -0
  118. package/dist/lib/sandbox.js +0 -2
  119. package/dist/lib/secrets-policy.d.ts +89 -11
  120. package/dist/lib/secrets-policy.js +175 -28
  121. package/dist/lib/session/db.d.ts +1 -1
  122. package/dist/lib/session/db.js +65 -18
  123. package/dist/lib/session/discover.js +1 -283
  124. package/dist/lib/session/mirror.d.ts +4 -3
  125. package/dist/lib/session/mirror.js +4 -3
  126. package/dist/lib/session/parse.d.ts +0 -2
  127. package/dist/lib/session/parse.js +1 -138
  128. package/dist/lib/session/throughput.d.ts +1 -3
  129. package/dist/lib/session/throughput.js +0 -20
  130. package/dist/lib/session/types.d.ts +2 -2
  131. package/dist/lib/session/types.js +2 -2
  132. package/dist/lib/sessions-client.d.ts +60 -1
  133. package/dist/lib/sessions-client.js +227 -4
  134. package/dist/lib/setup-tool-status.js +5 -10
  135. package/dist/lib/signin-badge.d.ts +3 -2
  136. package/dist/lib/signin-badge.js +6 -3
  137. package/dist/lib/smart-launch.js +4 -1
  138. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  139. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  140. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  141. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  142. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  143. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  144. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  147. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  148. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  149. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  150. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  151. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  152. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  153. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  154. package/dist/lib/staleness/writers/sources.js +5 -0
  155. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  156. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  157. package/dist/lib/state.d.ts +6 -4
  158. package/dist/lib/state.js +7 -17
  159. package/dist/lib/teams/parsers.d.ts +1 -1
  160. package/dist/lib/teams/parsers.js +1 -136
  161. package/dist/lib/teams/placement-probe.js +50 -15
  162. package/dist/lib/teams/scheduler.d.ts +7 -0
  163. package/dist/lib/types.d.ts +10 -1
  164. package/dist/lib/types.js +1 -1
  165. package/dist/lib/view-types.d.ts +32 -0
  166. package/dist/lib/watchdog/read.js +0 -1
  167. package/dist/lib/watchdog/rotate.js +1 -2
  168. package/dist/lib/watchdog/runner.js +2 -2
  169. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  170. package/dist/lib/watchdog/watchdogTail.js +0 -31
  171. package/package.json +1 -1
  172. package/scripts/postinstall.js +8 -8
  173. package/dist/browser.d.ts +0 -2
  174. package/dist/browser.js +0 -17
  175. package/dist/commands/browser-picker.d.ts +0 -18
  176. package/dist/commands/browser-picker.js +0 -97
  177. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  178. package/dist/lib/browser/arc-discovery.js +0 -197
  179. package/dist/lib/browser/arc-dom.d.ts +0 -14
  180. package/dist/lib/browser/arc-dom.js +0 -121
  181. package/dist/lib/browser/caller-identity.d.ts +0 -40
  182. package/dist/lib/browser/caller-identity.js +0 -175
  183. package/dist/lib/browser/cdp.d.ts +0 -51
  184. package/dist/lib/browser/cdp.js +0 -254
  185. package/dist/lib/browser/chrome.d.ts +0 -141
  186. package/dist/lib/browser/chrome.js +0 -704
  187. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  188. package/dist/lib/browser/chromium-discovery.js +0 -96
  189. package/dist/lib/browser/devices.d.ts +0 -34
  190. package/dist/lib/browser/devices.js +0 -61
  191. package/dist/lib/browser/domain-skills.d.ts +0 -71
  192. package/dist/lib/browser/domain-skills.js +0 -195
  193. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  194. package/dist/lib/browser/drivers/arc.js +0 -276
  195. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  196. package/dist/lib/browser/drivers/firefox.js +0 -377
  197. package/dist/lib/browser/drivers/local.d.ts +0 -55
  198. package/dist/lib/browser/drivers/local.js +0 -266
  199. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  200. package/dist/lib/browser/drivers/ssh.js +0 -467
  201. package/dist/lib/browser/editor.d.ts +0 -3
  202. package/dist/lib/browser/editor.js +0 -50
  203. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  204. package/dist/lib/browser/ffmpeg.js +0 -184
  205. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  206. package/dist/lib/browser/firefox-discovery.js +0 -162
  207. package/dist/lib/browser/har.d.ts +0 -85
  208. package/dist/lib/browser/har.js +0 -77
  209. package/dist/lib/browser/hygiene.d.ts +0 -97
  210. package/dist/lib/browser/hygiene.js +0 -153
  211. package/dist/lib/browser/index.d.ts +0 -5
  212. package/dist/lib/browser/index.js +0 -5
  213. package/dist/lib/browser/input.d.ts +0 -7
  214. package/dist/lib/browser/input.js +0 -92
  215. package/dist/lib/browser/ipc.d.ts +0 -204
  216. package/dist/lib/browser/ipc.js +0 -1453
  217. package/dist/lib/browser/login-detection.d.ts +0 -87
  218. package/dist/lib/browser/login-detection.js +0 -274
  219. package/dist/lib/browser/profiles.d.ts +0 -422
  220. package/dist/lib/browser/profiles.js +0 -1157
  221. package/dist/lib/browser/refs.d.ts +0 -89
  222. package/dist/lib/browser/refs.js +0 -191
  223. package/dist/lib/browser/registry.d.ts +0 -70
  224. package/dist/lib/browser/registry.js +0 -252
  225. package/dist/lib/browser/remote-control.d.ts +0 -59
  226. package/dist/lib/browser/remote-control.js +0 -85
  227. package/dist/lib/browser/resolve-target.d.ts +0 -66
  228. package/dist/lib/browser/resolve-target.js +0 -264
  229. package/dist/lib/browser/runtime-state.d.ts +0 -291
  230. package/dist/lib/browser/runtime-state.js +0 -584
  231. package/dist/lib/browser/secret-ref.d.ts +0 -10
  232. package/dist/lib/browser/secret-ref.js +0 -14
  233. package/dist/lib/browser/service.d.ts +0 -812
  234. package/dist/lib/browser/service.js +0 -4693
  235. package/dist/lib/browser/stream.d.ts +0 -17
  236. package/dist/lib/browser/stream.js +0 -72
  237. package/dist/lib/browser/task-index.d.ts +0 -78
  238. package/dist/lib/browser/task-index.js +0 -199
  239. package/dist/lib/browser/types.d.ts +0 -647
  240. package/dist/lib/browser/types.js +0 -61
  241. package/dist/lib/browser/upload.d.ts +0 -29
  242. package/dist/lib/browser/upload.js +0 -298
  243. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  244. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  245. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  246. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  247. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  248. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,29 +1,24 @@
1
1
  /**
2
- * Fleet shared-state sync as a `PeriodicService` (PHNX-3392 usage-sync,
3
- * PHNX-3792 session mirror).
2
+ * Fleet state exchange as a `PeriodicService` (PHNX-3392 usage-sync,
3
+ * PHNX-3792 session mirror, PHNX-4051 auth verdict, PHNX-4116 SSH transport).
4
4
  *
5
- * This is the ONE tick that owns the bounded Git exchange over the fleet-synced
6
- * user repo, so every non-secret daemon-state field rides it rather than opening
7
- * a second committer. Each tick: (1) publishes this box's own fields into its
8
- * conflict-free `devices/<device>/daemon-state.json` — a headed box's Claude
9
- * usage snapshot, EVERY box's lightweight session digests (PHNX-3792), and the
10
- * reserved-auth readiness verdict (PHNX-4051, folded in from the auth-sync tick
11
- * so a single caller holds the shared-repo lock per tick); (2) runs one
12
- * serialized, timeout-bounded commit/rebase/push; (3) consumes the peer fields
13
- * the exchange delivered — a worker merges usage newest-wins, and every
14
- * non-worker box folds peers' session digests into its local index so the picker
15
- * renders remote-host previews inline. No tick opens a device-to-device SSH mesh.
5
+ * Each tick: (1) refreshes every field this box owns in its own
6
+ * `devices/<device>/daemon-state.json` — a headed box's Claude usage snapshot,
7
+ * EVERY box's lightweight session digests, and the reserved-auth readiness
8
+ * verdict; (2) on a headed box (`personal`/`desktop`) only, dials every dialable
9
+ * peer in parallel with `agents __usage-ingest --reply`, sending that envelope
10
+ * on stdin and storing each peer's reply as `devices/<peer>/daemon-state.json`
11
+ * stamped `receivedAt` (`exchangeFleetStateWithPeers`); (3) folds the peers'
12
+ * session digests into the local index so the picker renders remote-host
13
+ * previews inline. A worker never initiates: its state leaves the box only as
14
+ * the reply to a headed peer's dial, and a headed peer's usage rows arrive on
15
+ * that same dial.
16
16
  *
17
- * Why the auth verdict publishes here (PHNX-4051): auth-sync used to run its OWN
18
- * `syncFleetSharedStateRepo`, so on every box two ticks 30 s apart contended for
19
- * the one `proper-lockfile` lock (20×100 ms ≈ 2 s of retries) while a real
20
- * fetch/rebase/push on a drifted repo runs far longer — the usage tick then
21
- * failed with "Lock file is already being held" (zion logged it 95× in 24 h) and
22
- * workers never received a fresh usage snapshot, which the 40-min placement gate
23
- * turned into "no ready device". Folding the auth verdict into this single
24
- * committer removes the second caller entirely. Auth-sync keeps its non-git
25
- * duties (worker-slot reconcile + the credential SSH pushes) under its own
26
- * deadline and circuit breaker.
17
+ * There is no git in this tick. The exchange used to be a commit/rebase/push of
18
+ * the fleet-synced user repo, which needed a cross-process lock, a 45 s
19
+ * process-tree deadline, an 8-minute kickoff offset from auth-sync to dodge that
20
+ * lock, and untracked-collision backups — and still wedged every clone behind a
21
+ * bloated remote. Peers that time out are skipped this tick; none blocks another.
27
22
  */
28
23
  import { BasePeriodicService } from './service.js';
29
24
  import { USAGE_SYNC_INTERVAL_MS } from '../accounting/usage-sync.js';
@@ -37,12 +32,8 @@ import { USAGE_SYNC_INTERVAL_MS } from '../accounting/usage-sync.js';
37
32
  */
38
33
  export const USAGE_SYNC_TICK_MS = USAGE_SYNC_INTERVAL_MS;
39
34
  const USAGE_SYNC_DEADLINE_MS = 2 * 60_000;
40
- /**
41
- * Offset from auth-sync's 60s kickoff so the two never contend for the
42
- * shared-repo lock. Auth fires at T+1m, T+16m, …; usage at T+8m, T+23m, …
43
- * The git exchange deadline is 45s, so a 7-minute gap is the whole point.
44
- */
45
- export const USAGE_SYNC_KICKOFF_MS = 8 * 60_000;
35
+ /** Let the daemon's registry/device probes settle before the first fan-out. */
36
+ export const USAGE_SYNC_KICKOFF_MS = 90_000;
46
37
  export class UsageSyncService extends BasePeriodicService {
47
38
  id = 'usage-sync';
48
39
  intervalMs = USAGE_SYNC_TICK_MS;
@@ -55,57 +46,34 @@ export class UsageSyncService extends BasePeriodicService {
55
46
  // Nothing to release — the supervisor's timer teardown is the only cleanup.
56
47
  }
57
48
  async onTick(ctx) {
58
- const { consumeUsageSnapshotsFromSharedStore, publishUsageSnapshotToSharedStore } = await import('../accounting/usage-sync.js');
59
- const { consumeSessionMirrorFromSharedStore, publishSessionMirrorToSharedStore } = await import('../session/mirror.js');
60
- // Publish every owned field BEFORE the single git exchange so they ride one commit.
61
- // Discovered native browser profiles (Arc Spaces, Comet profiles) go into this
62
- // device's declaration first, so every other box lists them with WHERE=<here>
63
- // and routes a `--profile` for one of them to this machine (PHNX-4042).
64
- const { publishDiscoveredProfiles } = await import('../browser/profiles.js');
65
- const profiles = await publishDiscoveredProfiles().catch((error) => ({
66
- published: [],
67
- errors: { publish: error instanceof Error ? error.message : String(error) },
68
- }));
69
- if (profiles.published.length > 0)
70
- ctx.log('INFO', `browser-profiles: published ${profiles.published.join(', ')} to this device's declaration`);
71
- for (const [browser, message] of Object.entries(profiles.errors))
72
- ctx.log('WARN', `browser-profiles: ${browser}: ${message}`);
73
- const published = await publishUsageSnapshotToSharedStore();
74
- if (published.changed)
75
- ctx.log('INFO', `usage-sync: published usage snapshot to ${published.path}`);
76
- if (published.error)
77
- ctx.log('WARN', `usage-sync: publish: ${published.error}`);
78
- const mirrored = await publishSessionMirrorToSharedStore();
79
- if (mirrored.changed)
80
- ctx.log('INFO', `session-mirror: published ${mirrored.count} session digest(s)`);
81
- if (mirrored.error)
82
- ctx.log('WARN', `session-mirror: publish: ${mirrored.error}`);
83
- // The reserved-auth readiness verdict rides this single git exchange too
84
- // (PHNX-4051): it is a conflict-free field in the same owned daemon-state
85
- // file, so publishing it here — instead of from a second committer in
86
- // auth-sync — is what keeps exactly one caller of syncFleetSharedStateRepo on
87
- // the periodic path.
88
- const { publishReservedAuthVerdict } = await import('../secrets-policy.js');
89
- const authVerdict = await publishReservedAuthVerdict();
90
- if (authVerdict.error)
91
- ctx.log('WARN', `usage-sync: auth verdict: ${authVerdict.error}`);
92
- const { syncFleetSharedStateRepo } = await import('../fleet-shared-repo-sync.js');
93
- const transport = await syncFleetSharedStateRepo();
94
- if (transport.skipped)
95
- ctx.log('WARN', `usage-sync: ${transport.skipped}`);
96
- if (transport.error)
97
- ctx.log('WARN', `usage-sync: shared-store transport: ${transport.error}`);
98
- if (transport.untrackedBackedUp?.length) {
99
- ctx.log('WARN', `usage-sync: backed up ${transport.untrackedBackedUp.length} untracked shared-store collision(s) to ${transport.untrackedBackupDir}: ${transport.untrackedBackedUp.join(', ')}`);
100
- }
101
- if (!transport.success)
49
+ const { exchangeFleetStateWithPeers, publishOwnFleetState } = await import('../accounting/usage-sync.js');
50
+ const { isHeadedDeviceRole, selfConfiguredDeviceRole } = await import('../device-config.js');
51
+ // Refresh every owned field BEFORE the fan-out so peers receive this tick's state.
52
+ const published = await publishOwnFleetState();
53
+ if (published.usage.changed)
54
+ ctx.log('INFO', `usage-sync: published usage snapshot to ${published.usage.path}`);
55
+ if (published.mirror.changed)
56
+ ctx.log('INFO', `session-mirror: published ${published.mirror.count} session digest(s)`);
57
+ for (const err of published.errors)
58
+ ctx.log('WARN', `usage-sync: publish ${err}`);
59
+ if (!isHeadedDeviceRole(selfConfiguredDeviceRole())) {
60
+ ctx.log('INFO', 'usage-sync: not a headed device; peers dial in with `agents __usage-ingest --reply`, nothing to send');
102
61
  return;
103
- const consumed = consumeUsageSnapshotsFromSharedStore();
104
- if (consumed.merged > 0) {
105
- ctx.log('INFO', `usage-sync: merged ${consumed.merged} row(s) from ${consumed.sources.join(', ')}`);
106
62
  }
107
- for (const err of consumed.errors)
108
- ctx.log('WARN', `usage-sync: ${err.device}: ${err.message}`);
63
+ const exchange = await exchangeFleetStateWithPeers();
64
+ if (exchange.skipped)
65
+ ctx.log('WARN', `usage-sync: ${exchange.skipped}`);
66
+ const delivered = exchange.outcomes.filter((o) => o.delivered);
67
+ if (delivered.length > 0) {
68
+ ctx.log('INFO', `usage-sync: exchanged with ${delivered.map((o) => `${o.device}${o.merged ? ` (+${o.merged} usage row(s))` : ''}`).join(', ')}`);
69
+ }
70
+ for (const o of exchange.outcomes) {
71
+ if (o.error)
72
+ ctx.log('WARN', `usage-sync: ${o.device}: ${o.error}`);
73
+ for (const err of o.peerErrors)
74
+ ctx.log('WARN', `usage-sync: ${o.device} reported: ${err}`);
75
+ }
76
+ const { consumeSessionMirrorFromSharedStore } = await import('../session/mirror.js');
109
77
  const foldedIn = consumeSessionMirrorFromSharedStore();
110
78
  if (foldedIn.merged > 0) {
111
79
  ctx.log('INFO', `session-mirror: folded ${foldedIn.merged} session(s) from ${foldedIn.sources.join(', ')}`);
@@ -1,5 +1,3 @@
1
- /** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
2
- export declare const SUBSYSTEM_BROWSER_IPC = "browser-ipc";
3
1
  /**
4
2
  * Daemon startup itself (RUSH-2418). Unlike the two above, this record is
5
3
  * written from BOTH sides: the launching CLI records a start that produced no
@@ -22,8 +20,8 @@ export interface SubsystemHealth {
22
20
  /** ISO timestamp of the most recent success, or null if it has never succeeded. */
23
21
  lastOkAt: string | null;
24
22
  /**
25
- * `ServiceSupervisor`'s lifecycle state (`idle`/`running`/`parked`/`stopped`),
26
- * written by `recordSubsystemState` on every transition. Only present for
23
+ * `ServiceSupervisor`'s lifecycle state (`idle`/`running`/`stopped`), written
24
+ * by `recordSubsystemState` on every transition. Only present for
27
25
  * supervisor-managed subsystems (RUSH-3193 P4) — a subsystem that predates the
28
26
  * supervisor (e.g. `daemon-start`) never has this field, which is how `agents
29
27
  * daemon services` tells a measured state from an inferred one.
@@ -50,10 +48,29 @@ export declare function recordSubsystemErrorReason(subsystem: string, error: str
50
48
  /**
51
49
  * Record a `ServiceSupervisor` lifecycle-state transition, without touching
52
50
  * the ok/error streak. Cross-process readers (`agents daemon services`) have
53
- * no other way to see `parked`/`stopped` vs `running` — `agents daemon
51
+ * no other way to see `stopped`/`idle` vs `running` — `agents daemon
54
52
  * status` runs as a separate process from the daemon (see module docblock).
55
53
  */
56
54
  export declare function recordSubsystemState(subsystem: string, state: string): void;
55
+ /** One supervised restart: the service whose deadline breach forced the daemon to exit, and when. */
56
+ export interface DaemonRestartRecord {
57
+ /** ISO timestamp of the breach that forced the restart. */
58
+ at: string;
59
+ /** The service id whose tick or lifecycle call breached its deadline. */
60
+ subsystem: string;
61
+ /** The deadline-breach message. */
62
+ cause: string;
63
+ }
64
+ /**
65
+ * Append a supervised-restart record and flush it to disk SYNCHRONOUSLY. The
66
+ * caller is `ServiceSupervisor.exitForRestart`, which calls `process.exit`
67
+ * immediately after, so the write must be durable before the process dies.
68
+ * Never throws (same contract as the health file above) — a dropped ledger entry
69
+ * must not keep the process from exiting for its restart.
70
+ */
71
+ export declare function recordDaemonRestart(subsystem: string, cause: string, at?: string): void;
72
+ /** Every supervised restart recorded at or after `sinceMs`, oldest first. */
73
+ export declare function readRecentDaemonRestarts(sinceMs: number): DaemonRestartRecord[];
57
74
  /** Read one subsystem's health record, or null if it has never reported in. */
58
75
  export declare function readSubsystemHealth(subsystem: string): SubsystemHealth | null;
59
76
  /** Read every subsystem's health record, sorted by subsystem name. */
@@ -27,8 +27,6 @@ import * as path from 'path';
27
27
  import { getDaemonDir } from './state.js';
28
28
  import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
29
29
  const HEALTH_FILE = 'health.json';
30
- /** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
31
- export const SUBSYSTEM_BROWSER_IPC = 'browser-ipc';
32
30
  /**
33
31
  * Daemon startup itself (RUSH-2418). Unlike the two above, this record is
34
32
  * written from BOTH sides: the launching CLI records a start that produced no
@@ -125,7 +123,7 @@ export function recordSubsystemErrorReason(subsystem, error, at = new Date().toI
125
123
  /**
126
124
  * Record a `ServiceSupervisor` lifecycle-state transition, without touching
127
125
  * the ok/error streak. Cross-process readers (`agents daemon services`) have
128
- * no other way to see `parked`/`stopped` vs `running` — `agents daemon
126
+ * no other way to see `stopped`/`idle` vs `running` — `agents daemon
129
127
  * status` runs as a separate process from the daemon (see module docblock).
130
128
  */
131
129
  export function recordSubsystemState(subsystem, state) {
@@ -134,6 +132,52 @@ export function recordSubsystemState(subsystem, state) {
134
132
  all[subsystem] = { ...existing, subsystem, state };
135
133
  });
136
134
  }
135
+ // ─── Supervised-restart ledger (PHNX-4116) ────────────────────────────────────
136
+ const RESTARTS_FILE = 'restarts.json';
137
+ /** Bound so a crash loop cannot grow the ledger file without limit. */
138
+ const MAX_RESTART_RECORDS = 200;
139
+ function getRestartsPath() {
140
+ return path.join(getDaemonDir(), RESTARTS_FILE);
141
+ }
142
+ function readRestarts() {
143
+ try {
144
+ const parsed = JSON.parse(fs.readFileSync(getRestartsPath(), 'utf-8'));
145
+ return Array.isArray(parsed) ? parsed : [];
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ }
151
+ /**
152
+ * Append a supervised-restart record and flush it to disk SYNCHRONOUSLY. The
153
+ * caller is `ServiceSupervisor.exitForRestart`, which calls `process.exit`
154
+ * immediately after, so the write must be durable before the process dies.
155
+ * Never throws (same contract as the health file above) — a dropped ledger entry
156
+ * must not keep the process from exiting for its restart.
157
+ */
158
+ export function recordDaemonRestart(subsystem, cause, at = new Date().toISOString()) {
159
+ try {
160
+ const restartsPath = getRestartsPath();
161
+ ensureLockTarget(restartsPath, '[]');
162
+ withFileLock(restartsPath, () => {
163
+ const records = readRestarts();
164
+ records.push({ at, subsystem, cause });
165
+ atomicWriteFileSync(restartsPath, JSON.stringify(records.slice(-MAX_RESTART_RECORDS)), { encoding: 'utf-8', mode: 0o600 });
166
+ try {
167
+ fs.chmodSync(restartsPath, 0o600);
168
+ }
169
+ catch { /* best effort */ }
170
+ });
171
+ }
172
+ catch { /* a dropped restart record must never block the exit-for-restart */ }
173
+ }
174
+ /** Every supervised restart recorded at or after `sinceMs`, oldest first. */
175
+ export function readRecentDaemonRestarts(sinceMs) {
176
+ return readRestarts().filter((r) => {
177
+ const t = Date.parse(r.at);
178
+ return Number.isFinite(t) && t >= sinceMs;
179
+ });
180
+ }
137
181
  /** Read one subsystem's health record, or null if it has never reported in. */
138
182
  export function readSubsystemHealth(subsystem) {
139
183
  return readAll()[subsystem] ?? null;
@@ -7,7 +7,7 @@
7
7
  * enabled without pulling in the whole daemon lifecycle.
8
8
  */
9
9
  /** Every service the daemon can host. IDs are kebab-case and stable. */
10
- export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'session-title' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'feed-stream' | 'session-summarizer' | 'attention-notify' | 'harness-update';
10
+ export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'session-title' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'session-state' | 'feed-stream' | 'session-summarizer' | 'attention-notify' | 'harness-update';
11
11
  /** Human-readable metadata for each service. */
12
12
  interface DaemonServiceDef {
13
13
  id: DaemonServiceId;
@@ -27,11 +27,6 @@ export const DAEMON_SERVICES = [
27
27
  title: 'Monitor engine',
28
28
  description: 'Watches event sources and triggers monitor-driven routines.',
29
29
  },
30
- {
31
- id: 'browser-ipc',
32
- title: 'Browser IPC',
33
- description: 'Keeps a supervised browser automation IPC socket available for sessions.',
34
- },
35
30
  {
36
31
  id: 'webhook-receiver',
37
32
  title: 'Webhook receiver',
@@ -82,11 +77,6 @@ export const DAEMON_SERVICES = [
82
77
  title: 'Tmux reap',
83
78
  description: 'Reaps dead managed tmux sessions and their orphaned helper processes.',
84
79
  },
85
- {
86
- id: 'browser-task-reap',
87
- title: 'Browser task reap',
88
- description: 'Closes abandoned browser-task tabs whose owner exited or idle lease expired.',
89
- },
90
80
  {
91
81
  id: 'session-state',
92
82
  title: 'Live session state',
@@ -158,9 +158,11 @@ export async function runUsageRefreshTick(signal) {
158
158
  // is bounded by deadlineMs, not just each fetch's own 5s timeout (PHNX-3608).
159
159
  signal,
160
160
  pollerDevice: machineId(),
161
+ // A changed poll refreshes this box's own state file so the next usage-sync
162
+ // fan-out (or a placement probe before it) sends the new reading.
161
163
  onSnapshotsChanged: async () => {
162
- const { pushUsageSnapshotNow } = await import('./accounting/usage-sync.js');
163
- await pushUsageSnapshotNow();
164
+ const { publishUsageSnapshotToSharedStore } = await import('./accounting/usage-sync.js');
165
+ await publishUsageSnapshotToSharedStore();
164
166
  },
165
167
  });
166
168
  const { listProfiles } = await import('./profiles.js');
@@ -315,12 +315,7 @@ export function remediationFor(finding) {
315
315
  // shape only, for a kind constructed through finding() by a future caller.
316
316
  return 'kill <pid>';
317
317
  case 'owner-sink-unreachable':
318
- // The lane delivers over the rush-backed owner channel, which needs rush on
319
- // PATH AND a usable session in THIS context. Non-interactive shells miss a
320
- // ~/.zshrc export (RUSH-2258), and the session is keychain-bound, not in
321
- // ~/.rush/user.yaml (RUSH-2262) — so `rush login` here, or the Rush App for
322
- // a GUI keychain, is what makes it reachable.
323
- return "put rush on PATH for non-interactive shells (~/.zshenv) and run 'rush login'";
318
+ return 'check the channel transport: iMessage needs macOS, Slack needs SLACK_BOT_TOKEN in env or the webhooks bundle';
324
319
  }
325
320
  }
326
321
  function finding(f) {
@@ -369,11 +364,13 @@ export function buildLocalFindings(input) {
369
364
  const sink = input.ownerSink;
370
365
  if (sink?.configured && !sink.reachable) {
371
366
  const chan = sink.channel ?? 'owner';
372
- const why = sink.reason === 'rush-not-on-path'
373
- ? `rush CLI not on this box's PATH`
374
- : sink.reason === 'rush-signed-out'
375
- ? 'rush has no usable session here'
376
- : 'transport unreachable';
367
+ const why = sink.reason === 'imessage-not-macos'
368
+ ? 'iMessage requires macOS (peer-forward delivers from a macOS peer)'
369
+ : sink.reason === 'slack-no-token'
370
+ ? 'no SLACK_BOT_TOKEN — set it in env or the webhooks secrets bundle'
371
+ : sink.reason === 'channel-unsupported'
372
+ ? `${chan} transport removed (Rush daemon)`
373
+ : 'transport unreachable';
377
374
  out.push(finding({
378
375
  severity: FINDING_SEVERITY['owner-sink-unreachable'], kind: 'owner-sink-unreachable', device,
379
376
  message: `${chan} → owner unreachable: ${why}`,
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import chalk from 'chalk';
17
17
  import { ALL_AGENT_IDS, accountDisplayLabel, getAccountInfo, } from '../agents.js';
18
- import { deriveUsageStatusFromSnapshot, classifyUsageErrorKind, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
18
+ import { deriveUsageStatusFromSnapshot, liveUsageWindows, classifyUsageErrorKind, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
19
19
  import { getVersionHomePath, listInstalledVersions } from '../installations/store.js';
20
20
  import { readMeta } from '../state.js';
21
21
  import { findNativeAccountByIdentity } from '../account-registry.js';
@@ -59,11 +59,20 @@ export function summarizeQuota(snapshot, unavailableReason = null, accountStatus
59
59
  unavailableReason: status ? null : (reason ?? 'usage unavailable'),
60
60
  };
61
61
  }
62
- const blocking = snapshot.windows.filter((w) => w.key !== 'sonnet_week');
63
- const windows = blocking.length > 0 ? blocking : snapshot.windows;
62
+ // Percentage and status MUST read from the SAME live windows, or a window past
63
+ // its reset (usedPercent from the PREVIOUS period) makes the status `available`
64
+ // while the displayed percentage still shows a stale ~100% (#3705). Select the
65
+ // live set first, then narrow to blocking exactly as
66
+ // `deriveUsageStatusFromSnapshot` does.
67
+ const live = liveUsageWindows(snapshot);
68
+ const blocking = live.filter((w) => w.key !== 'sonnet_week');
69
+ const windows = blocking.length > 0 ? blocking : live;
64
70
  const derived = deriveUsageStatusFromSnapshot(snapshot);
65
71
  const status = accountStatus === 'out_of_credits' ? accountStatus : derived;
66
- let usedPercent = Math.round(Math.max(...windows.map((w) => w.usedPercent)));
72
+ // Every window rolled over: no live utilization to show (status is `available`).
73
+ let usedPercent = windows.length > 0
74
+ ? Math.round(Math.max(...windows.map((w) => w.usedPercent)))
75
+ : 0;
67
76
  // Never show 100% for an account that isn't actually capped: a genuinely-100
68
77
  // blocking window makes the status `rate_limited` (rendered "limited"), so a
69
78
  // rounded 100 on an `available` account (e.g. 99.6% → 100) would read as maxed
@@ -123,7 +132,7 @@ export function applyUsageHonesty(verdict, usage) {
123
132
  }
124
133
  return { verdict, usage: stripped };
125
134
  }
126
- if ((usage.status === 'rate_limited' || usage.status === 'out_of_credits') && (verdict === 'live' || verdict === 'unverified')) {
135
+ if ((usage.status === 'rate_limited' || usage.status === 'out_of_credits') && (verdict === 'live' || verdict === 'unverified' || verdict === 'no_evidence')) {
127
136
  return { verdict: 'rate_limited', usage };
128
137
  }
129
138
  return { verdict, usage };
@@ -142,6 +142,11 @@ async function matchLegacyIdentityHome(agent, account, meta) {
142
142
  const needles = [row?.identityKey, row?.identityLabel]
143
143
  .filter((value) => typeof value === 'string' && value.trim().length > 0)
144
144
  .map((value) => value.trim().toLowerCase());
145
+ // For accounts that predate the native registry, the row is missing but the
146
+ // account name carries the email the caller selected with `#email`.
147
+ if (needles.length === 0 && account.name.includes('@')) {
148
+ needles.push(account.name.trim().toLowerCase());
149
+ }
145
150
  if (needles.length === 0)
146
151
  return null;
147
152
  const needleSet = new Set(needles);
package/dist/lib/exec.js CHANGED
@@ -541,17 +541,6 @@ export const AGENT_COMMANDS = {
541
541
  jsonFlags: ['--json'],
542
542
  modelFlag: '--model',
543
543
  },
544
- gemini: {
545
- base: ['gemini'],
546
- promptFlag: 'positional',
547
- modeFlags: {
548
- plan: ['--approval-mode', 'plan'],
549
- edit: ['--approval-mode', 'auto_edit'],
550
- skip: ['--yolo'],
551
- },
552
- jsonFlags: ['--output-format', 'stream-json'],
553
- modelFlag: '--model',
554
- },
555
544
  cursor: {
556
545
  base: ['cursor-agent'],
557
546
  promptFlag: '-p',
@@ -469,7 +469,6 @@ function sanitizePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
469
469
  const TERMINAL_CALLERS = {
470
470
  cc: 'claude', cl: 'claude',
471
471
  cx: 'codex',
472
- gx: 'gemini', gm: 'gemini',
473
472
  cr: 'cursor',
474
473
  oc: 'opencode',
475
474
  sh: 'shell',
@@ -28,7 +28,6 @@ import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
29
29
  import { getBrowserRuntimeDir } from '../state.js';
30
30
  import { getEventsDir } from './events.js';
31
- import { listTaskBindings } from '../browser/task-index.js';
32
31
  import { buildBrowserSessionRows } from '../browser/sessions-list.js';
33
32
  import { buildComputerSessionRows, standaloneComputerActionsDir } from '../computer/sessions-list.js';
34
33
  import { boundBrowserRow, projectBrowserToolRow, projectComputerToolRow, sortToolRows } from './tools.js';
@@ -52,8 +51,12 @@ export function collectToolRows(scope, sources = {}) {
52
51
  return empty;
53
52
  }
54
53
  };
54
+ // Task→device binding is browser-cli's now (bound at `start`, PHNX-4101), so
55
+ // agents-cli keeps no separate binding index. Cross-device tasks are driven by
56
+ // browser-cli from THIS box, so their `tasks.json` is local and reached by
57
+ // `readLiveBrowserTasks` below. The `bindings` source stays a test seam.
55
58
  const bindings = new Map();
56
- for (const binding of read(sources.bindings ?? listTaskBindings, []))
59
+ for (const binding of read(sources.bindings ?? (() => []), []))
57
60
  bindings.set(binding.name, binding);
58
61
  const liveTasks = new Map();
59
62
  for (const task of read(sources.liveTasks ?? (() => readLiveBrowserTasks()), []))
@@ -56,11 +56,27 @@ export interface FleetSharedDeviceState {
56
56
  sessions?: {
57
57
  rows: SessionMirrorRow[];
58
58
  };
59
+ /**
60
+ * Per-account auth verdict rows, written by the account-state daemon service
61
+ * (`account-state-daemon-service.ts`) and read by `readSharedAccountVerdicts`.
62
+ * Opaque here: the envelope carries them across the exchange unchanged.
63
+ */
64
+ accounts?: {
65
+ rows: unknown[];
66
+ };
67
+ /**
68
+ * Epoch ms this box received the envelope from its owner over the SSH
69
+ * exchange. Present ONLY in a peer's file on this box, never in a device's own
70
+ * file. auth-sync reads it per peer to know that peer has replied at all.
71
+ */
72
+ receivedAt?: number;
59
73
  }
60
74
  interface FleetSharedStatePatch {
61
75
  usage?: FleetSharedDeviceState['usage'];
62
76
  auth?: FleetSharedDeviceState['auth'];
63
77
  sessions?: FleetSharedDeviceState['sessions'];
78
+ accounts?: FleetSharedDeviceState['accounts'];
79
+ receivedAt?: number;
64
80
  }
65
81
  interface FleetSharedStateReadResult {
66
82
  states: FleetSharedDeviceState[];
@@ -71,6 +87,13 @@ interface FleetSharedStateReadResult {
71
87
  }
72
88
  /** Path owned by one device in the conflict-free tracked device-doc tree. */
73
89
  export declare function fleetSharedStatePath(device: string, userAgentsDir?: string): string;
90
+ /**
91
+ * Validate an already-parsed envelope. `owner` pins the `device` field when the
92
+ * caller knows whose envelope this must be (a file under `devices/<owner>/`);
93
+ * an envelope arriving over the exchange names its own owner, so the caller
94
+ * passes none and checks `device` against the dialed peer afterwards.
95
+ */
96
+ export declare function parseFleetSharedDeviceStateEnvelope(parsed: unknown, owner?: string): FleetSharedDeviceState;
74
97
  export declare function updateFleetSharedDeviceState(device: string, patch: FleetSharedStatePatch, userAgentsDir?: string): {
75
98
  changed: boolean;
76
99
  path: string;
@@ -89,4 +112,26 @@ export declare function updateFleetSharedDeviceStateAsync(device: string, patch:
89
112
  }>;
90
113
  /** Read every valid peer-owned state file; malformed peers fail separately. */
91
114
  export declare function readFleetSharedDeviceStates(userAgentsDir?: string): FleetSharedStateReadResult;
115
+ /**
116
+ * This device's own envelope as it stands on disk — what the usage-sync exchange
117
+ * sends to peers and what `__usage-ingest --reply` prints back. A device that has
118
+ * never published anything yields the bare `{version, device}` envelope, so a
119
+ * peer still learns the device exists and answered.
120
+ */
121
+ export declare function readOwnFleetSharedDeviceState(device: string, userAgentsDir?: string): FleetSharedDeviceState;
122
+ /**
123
+ * Store a PEER's envelope, received over the SSH exchange, in that peer's file on
124
+ * this box, stamped with `receivedAt`. Merges field-by-field onto whatever the
125
+ * file already holds, so a partial envelope (a placement probe sends usage only)
126
+ * refreshes that field without erasing the peer's last auth verdict, session
127
+ * digests, or account rows. The five readers of `devices/<peer>/daemon-state.json`
128
+ * (usage merge, poller claims, auth verdicts, session mirror, account catalog)
129
+ * are unchanged by the transport swap. Async because it runs on the daemon's
130
+ * usage-sync tick once per peer reply: the sync lock would `sleepSync` the event
131
+ * loop for up to 30 s under contention (see {@link updateFleetSharedDeviceStateAsync}).
132
+ */
133
+ export declare function storePeerFleetSharedDeviceState(state: FleetSharedDeviceState, userAgentsDir?: string, receivedAt?: number): Promise<{
134
+ changed: boolean;
135
+ path: string;
136
+ }>;
92
137
  export {};