@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
@@ -1,35 +1,80 @@
1
1
  /**
2
- * The `agents __usage-ingest` receiver (PHNX-3392 usage-sync).
2
+ * The `agents __usage-ingest` receiver — the worker side of the usage exchange
3
+ * (PHNX-3392 usage-sync, PHNX-4116 transport over SSH).
3
4
  *
4
- * A legacy headed peer may pipe a {@link UsageSyncPayload} JSON envelope to our
5
- * stdin; we merge its identity-keyed rows into the local usage cache newest-wins
6
- * ({@link ingestPeerClaudeUsageRows}). Hidden internal verb — intercepted in
7
- * index.ts before bootstrap, so it never triggers an update check or a detached
8
- * sync, and it writes NOTHING to stdout (the caller only reads the exit code).
5
+ * A headed peer pipes its daemon-state envelope ({@link FleetStateExchangePayload},
6
+ * v2) to our stdin. We store it as that peer's `devices/<peer>/daemon-state.json`
7
+ * (stamped `receivedAt`) and merge its usage rows into the local cache
8
+ * newest-wins ({@link applyPeerFleetState}). With `--reply` we then refresh our
9
+ * own fields (session digests, reserved-auth verdict, and usage if this box is
10
+ * headed) and print our own envelope to stdout after a marker line, so the
11
+ * headed box holds this device's state without a second round-trip. Without
12
+ * `--reply` nothing is written to stdout: the ready probe runs this verb ahead
13
+ * of `agents --version`/`agents view --json` in one shell and parses that
14
+ * stdout, so a silent ingest is what keeps the probe parseable.
9
15
  *
10
- * Exit codes: 0 = merged (or nothing to merge — an empty payload is not an error),
11
- * 2 = malformed input. It fails loud on a bad envelope rather than silently
12
- * accepting a wrong shape, but a busy cache lock degrades to best-effort inside
13
- * `ingestPeerClaudeUsageRows` like every other cache writer.
16
+ * The legacy v1 envelope (`{v:1, rows}`) from an older headed peer is still
17
+ * merged. Hidden internal verb — intercepted in index.ts before bootstrap, so
18
+ * it never triggers an update check or a detached sync.
14
19
  *
15
- * New daemon ticks use the fleet-shared store instead. This compatibility
16
- * receiver remains for older installed versions. The payload arrives on stdin,
17
- * EXCEPT on a Windows receiver: the `agents.ps1`
18
- * shim does not forward ssh-piped stdin to the node process, so the pusher writes
19
- * the payload to a temp file and passes `agents __usage-ingest --from <path>`
20
- * (the same workaround the secrets push uses — `buildWindowsStdinImportCommand`).
20
+ * Exit codes: 0 = applied (or nothing to apply — an empty payload is not an
21
+ * error), 2 = malformed or oversized input. It fails loud on a bad envelope
22
+ * rather than silently accepting a wrong shape, but a busy cache lock degrades
23
+ * to best-effort inside `ingestPeerClaudeUsageRows` like every other cache
24
+ * writer. Stdin is bounded at {@link REMOTE_STDOUT_MAX_BYTES} — the same ceiling
25
+ * the dialer puts on a peer's reply — so a runaway pusher cannot grow this
26
+ * every-15-min receiver's heap; an overflow stops the read, exits 2, and writes
27
+ * nothing.
28
+ *
29
+ * The payload arrives on stdin, EXCEPT on a Windows receiver: the `agents.ps1`
30
+ * shim does not forward ssh-piped stdin to the node process, so the pusher
31
+ * writes the payload to a temp file and passes `--from <path>`
32
+ * (`buildWindowsStdinAgentsCommand`).
21
33
  */
22
34
  import * as fs from 'fs';
35
+ import { REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator } from '../ssh-exec.js';
23
36
  import { ingestPeerClaudeUsageRows } from './usage.js';
24
- function readStdin() {
25
- return new Promise((resolve) => {
26
- let buf = '';
27
- process.stdin.setEncoding('utf-8');
37
+ import { applyPeerFleetState, buildFleetStatePayload, formatFleetStateReply, parseFleetStateExchangeInput, publishOwnFleetState, } from './usage-sync.js';
38
+ /** Stdin grew past {@link REMOTE_STDOUT_MAX_BYTES}; the payload was refused unread. */
39
+ export class UsageIngestInputTooLargeError extends Error {
40
+ limitBytes;
41
+ constructor(limitBytes) {
42
+ super(`stdin payload exceeds ${limitBytes} bytes (${limitBytes / (1024 * 1024)} MiB); refusing it unread`);
43
+ this.limitBytes = limitBytes;
44
+ this.name = 'UsageIngestInputTooLargeError';
45
+ }
46
+ }
47
+ /**
48
+ * Read stdin to EOF, bounded at {@link REMOTE_STDOUT_MAX_BYTES}. On overflow the
49
+ * stream is destroyed so the pusher gets EPIPE instead of a full drain, and the
50
+ * promise rejects with {@link UsageIngestInputTooLargeError} — the caller exits
51
+ * 2 without parsing or writing anything.
52
+ */
53
+ function readStdin(limitBytes = REMOTE_STDOUT_MAX_BYTES) {
54
+ return new Promise((resolve, reject) => {
55
+ const acc = new RemoteUtf8Accumulator();
56
+ let bytes = 0;
57
+ let settled = false;
28
58
  process.stdin.on('data', (chunk) => {
29
- buf += chunk;
59
+ if (settled)
60
+ return;
61
+ bytes += chunk.byteLength;
62
+ if (bytes > limitBytes) {
63
+ settled = true;
64
+ process.stdin.destroy();
65
+ reject(new UsageIngestInputTooLargeError(limitBytes));
66
+ return;
67
+ }
68
+ acc.write(chunk);
30
69
  });
31
- process.stdin.on('end', () => resolve(buf));
32
- process.stdin.on('error', () => resolve(buf));
70
+ process.stdin.on('end', () => { if (!settled) {
71
+ settled = true;
72
+ resolve(acc.end());
73
+ } });
74
+ process.stdin.on('error', () => { if (!settled) {
75
+ settled = true;
76
+ resolve(acc.end());
77
+ } });
33
78
  });
34
79
  }
35
80
  /** `--from <path>` reads the payload from a file instead of stdin (Windows path). */
@@ -37,8 +82,9 @@ function fromFileArg(argv) {
37
82
  const i = argv.indexOf('--from');
38
83
  return i !== -1 && argv[i + 1] ? argv[i + 1] : null;
39
84
  }
40
- export async function runUsageIngest() {
41
- const fromPath = fromFileArg(process.argv.slice(3));
85
+ export async function runUsageIngest(argv = process.argv.slice(3)) {
86
+ const reply = argv.includes('--reply');
87
+ const fromPath = fromFileArg(argv);
42
88
  let source;
43
89
  if (fromPath) {
44
90
  try {
@@ -50,28 +96,52 @@ export async function runUsageIngest() {
50
96
  }
51
97
  }
52
98
  else {
53
- source = await readStdin();
99
+ try {
100
+ source = await readStdin();
101
+ }
102
+ catch (err) {
103
+ if (!(err instanceof UsageIngestInputTooLargeError))
104
+ throw err;
105
+ process.stderr.write(`[agents] __usage-ingest: ${err.name}: ${err.message}\n`);
106
+ return 2;
107
+ }
54
108
  }
55
109
  const raw = source.trim();
56
- if (!raw)
57
- return 0; // nothing piped — a no-op tick, not a failure.
58
- let payload;
59
- try {
60
- payload = JSON.parse(raw);
61
- }
62
- catch {
63
- process.stderr.write('[agents] __usage-ingest: malformed JSON payload\n');
64
- return 2;
65
- }
66
- if (!payload ||
67
- payload.v !== 1 ||
68
- typeof payload.rows !== 'object' ||
69
- payload.rows === null ||
70
- Array.isArray(payload.rows) // `typeof [] === 'object'` — an array is NOT a rows map
71
- ) {
72
- process.stderr.write('[agents] __usage-ingest: unrecognized usage-sync payload shape\n');
73
- return 2;
110
+ const errors = [];
111
+ if (raw) {
112
+ let payload;
113
+ try {
114
+ payload = parseFleetStateExchangeInput(raw);
115
+ }
116
+ catch (err) {
117
+ process.stderr.write(`[agents] __usage-ingest: ${err.message}\n`);
118
+ return 2;
119
+ }
120
+ if (payload.v === 1) {
121
+ await ingestPeerClaudeUsageRows(payload.rows);
122
+ }
123
+ else {
124
+ try {
125
+ await applyPeerFleetState(payload.state);
126
+ }
127
+ catch (err) {
128
+ // A well-formed envelope we refuse (it names this device) is the
129
+ // sender's mistake, not malformed input: say so on stderr and in the
130
+ // reply, and still answer so the sender learns what this box holds.
131
+ const message = err.message;
132
+ process.stderr.write(`[agents] __usage-ingest: ${message}\n`);
133
+ errors.push(`ingest: ${message}`);
134
+ if (!reply)
135
+ return 2;
136
+ }
137
+ }
74
138
  }
75
- ingestPeerClaudeUsageRows(payload.rows);
139
+ if (!reply)
140
+ return 0;
141
+ const published = await publishOwnFleetState();
142
+ errors.push(...published.errors);
143
+ for (const message of published.errors)
144
+ process.stderr.write(`[agents] __usage-ingest: ${message}\n`);
145
+ process.stdout.write(formatFleetStateReply(buildFleetStatePayload({ errors })));
76
146
  return 0;
77
147
  }
@@ -1,29 +1,59 @@
1
1
  /**
2
- * Fleet distribution of identity-keyed Claude usage through the user repo.
2
+ * Fleet distribution of identity-keyed Claude usage over SSH (PHNX-4116).
3
3
  *
4
- * A headed device can read authoritative usage; a worker's setup-token cannot.
5
- * Headed daemons therefore publish one snapshot into their conflict-free
6
- * `~/.agents/devices/<device>/daemon-state.json`. The daemon's bounded Git
7
- * transport delivers those files automatically; workers merge newest-wins.
8
- * There is deliberately no device-to-device SSH in this module or its tick.
4
+ * A headed device can read authoritative usage; a worker's setup-token cannot
5
+ * (credential-management.md invariant 5: a worker never polls
6
+ * `/api/oauth/usage`). So the headed daemon's `usage-sync` tick dials every
7
+ * dialable peer in parallel with `agents __usage-ingest --reply`, sending this
8
+ * box's own daemon-state envelope on stdin; the peer merges the usage rows into
9
+ * its cache newest-wins and prints its own envelope back, which the headed box
10
+ * stores in `~/.agents/devices/<peer>/daemon-state.json` stamped `receivedAt`.
11
+ * Workers never initiate. A peer that times out is skipped this tick and never
12
+ * blocks another. Only usage rows, verdicts, and session digests move here —
13
+ * never a credential (invariant 7: roles never cross).
14
+ *
15
+ * This replaced a git exchange over the fleet-synced user repo: every daemon
16
+ * committed its own file each tick, so the shared store reached 1.1 GiB /
17
+ * 18,358 commits with 100% of the last 2,000 being `chore(devices): publish
18
+ * <device> daemon state`, and a worker's clone fell 10k commits behind with
19
+ * `git fetch timed out` on every tick — a valid setup-token could not be
20
+ * scheduled because a repo was bloated. Git now carries human-authored
21
+ * resources only; `devices/<device>/daemon-state.json` is untracked.
9
22
  */
10
23
  import { type ConfiguredDeviceRole } from '../device-config.js';
24
+ import { type FleetSharedDeviceState } from '../fleet-shared-state.js';
25
+ import type { DeviceProfile } from '../devices/registry.js';
26
+ import type { SshExecResult } from '../ssh-exec.js';
11
27
  import { type CachedUsageSnapshot } from './usage.js';
12
- /** Cadence of the usage-sync git tick — the trust window for a `sync` snapshot. */
28
+ /** Cadence of the usage-sync tick — the trust window for a `sync` snapshot. */
13
29
  export declare const USAGE_SYNC_INTERVAL_MS: number;
14
30
  /** Re-publish an unchanged windowed row at least this often so workers' 15-min trust does not lapse. */
15
31
  export declare const USAGE_PUBLISH_HEARTBEAT_MS: number;
32
+ /** Per-peer SSH budget for one exchange; a peer past it is skipped this tick. */
33
+ export declare const USAGE_EXCHANGE_PEER_TIMEOUT_MS = 20000;
34
+ /** Marks the start of the JSON reply on the peer's stdout, so login-shell noise before it is ignored. */
35
+ export declare const FLEET_STATE_REPLY_MARKER = "@@AGENTS_FLEET_STATE@@";
16
36
  /**
17
37
  * Keep the previously published `capturedAt` when meters have not moved and
18
38
  * the last publish is still inside the heartbeat. Stops statusline re-renders
19
- * from dirtying the shared store (and taking the git lock) every few seconds.
39
+ * from rewriting the own-state file every few seconds.
20
40
  */
21
41
  export declare function mergeUsageRowsForPublish(previous: Record<string, CachedUsageSnapshot> | undefined, next: Record<string, CachedUsageSnapshot>, nowMs?: number): Record<string, CachedUsageSnapshot>;
22
- /** Legacy hidden ingest/export envelope kept for older fleet CLI compatibility. */
42
+ /** Legacy `__usage-ingest` envelope (v1): bare rows from an older headed peer. */
23
43
  export interface UsageSyncPayload {
24
44
  v: 1;
25
45
  rows: Record<string, CachedUsageSnapshot>;
26
46
  }
47
+ /**
48
+ * The exchange envelope (v2), identical in both directions: a device's own
49
+ * daemon-state envelope. `errors` carries the replying peer's non-fatal
50
+ * publisher failures so the headed box can log them against that peer.
51
+ */
52
+ export interface FleetStateExchangePayload {
53
+ v: 2;
54
+ state: FleetSharedDeviceState;
55
+ errors?: string[];
56
+ }
27
57
  interface PublishUsageSnapshotOptions {
28
58
  userAgentsDir?: string;
29
59
  cachePath?: string;
@@ -37,39 +67,112 @@ interface PublishUsageSnapshotResult {
37
67
  error: string | null;
38
68
  path: string | null;
39
69
  }
40
- /** Publish this headed device's stable usage snapshot into its owned store file. */
70
+ /** Publish this headed device's stable usage snapshot into its own state file. */
41
71
  export declare function publishUsageSnapshotToSharedStore(options?: PublishUsageSnapshotOptions): Promise<PublishUsageSnapshotResult>;
72
+ export interface PublishOwnFleetStateResult {
73
+ usage: PublishUsageSnapshotResult;
74
+ mirror: {
75
+ changed: boolean;
76
+ count: number;
77
+ error: string | null;
78
+ };
79
+ auth: {
80
+ status: string;
81
+ error: string | null;
82
+ };
83
+ /** Every publisher failure, as `<field>: <message>` — logged by the caller, never fatal. */
84
+ errors: string[];
85
+ }
42
86
  /**
43
- * Publish a changed snapshot immediately and exchange the shared repo so
44
- * workers see it without waiting for the 15-minute tick. No-ops when the
45
- * serialized store is unchanged (statusline re-renders with the same windows).
87
+ * Refresh every field this device owns in its own state file: the usage
88
+ * snapshot (headed only), the session digests, and the reserved-auth readiness
89
+ * verdict. Run before the envelope is sent (the headed tick) or printed back
90
+ * (`__usage-ingest --reply`) so a peer always receives current fields. Each
91
+ * publisher is independent: one failing is reported in `errors` and the others
92
+ * still land. All three writers are the async, non-blocking variants
93
+ * (`updateFleetSharedDeviceStateAsync` underneath).
46
94
  */
47
- export declare function pushUsageSnapshotNow(options?: PublishUsageSnapshotOptions & {
48
- lockPath?: string;
49
- timeoutMs?: number;
50
- }): Promise<{
51
- published: PublishUsageSnapshotResult;
52
- transport?: import('../fleet-shared-repo-sync.js').FleetSharedRepoSyncResult;
53
- }>;
54
- interface ConsumeUsageSnapshotsOptions {
95
+ export declare function publishOwnFleetState(options?: PublishUsageSnapshotOptions): Promise<PublishOwnFleetStateResult>;
96
+ interface BuildPayloadOptions {
97
+ device?: string;
55
98
  userAgentsDir?: string;
56
99
  cachePath?: string;
57
100
  role?: ConfiguredDeviceRole;
101
+ /**
102
+ * Send only the usage rows (read live from the cache, headed only) — the
103
+ * placement probe's shape: the dispatcher hands the chosen worker current
104
+ * numbers without the sessions/auth fields the tick carries.
105
+ */
106
+ usageOnly?: boolean;
107
+ errors?: string[];
108
+ }
109
+ /** This device's exchange envelope, from its own state file (or usage-only from the cache). */
110
+ export declare function buildFleetStatePayload(options?: BuildPayloadOptions): FleetStateExchangePayload;
111
+ /** Serialize the reply as the peer prints it: marker line, then one JSON line. */
112
+ export declare function formatFleetStateReply(payload: FleetStateExchangePayload): string;
113
+ /**
114
+ * Parse what a peer sent — a v2 exchange envelope, or the legacy v1 bare-rows
115
+ * envelope from an older headed peer. Throws a clear message on anything else;
116
+ * the caller decides whether that is exit code 2 (the receiver) or a per-peer
117
+ * error (the headed tick).
118
+ */
119
+ export declare function parseFleetStateExchangeInput(raw: string): FleetStateExchangePayload | UsageSyncPayload;
120
+ /** Extract the reply envelope from a peer's stdout (text before the marker is login-shell noise). */
121
+ export declare function parseFleetStateReply(stdout: string): FleetStateExchangePayload;
122
+ interface ApplyPeerStateOptions {
123
+ userAgentsDir?: string;
124
+ cachePath?: string;
58
125
  device?: string;
59
- roles?: Record<string, ConfiguredDeviceRole>;
126
+ receivedAt?: number;
60
127
  }
61
- interface ConsumeUsageSnapshotsResult {
62
- sources: string[];
128
+ export interface ApplyPeerStateResult {
129
+ path: string;
130
+ /** Usage rows merged into the local cache (newest-wins per identity). */
63
131
  merged: number;
132
+ receivedAt: number;
133
+ }
134
+ /**
135
+ * Take a peer's envelope in: store it as that peer's file (stamped `receivedAt`)
136
+ * and merge its usage rows into this box's cache newest-wins. The same step runs
137
+ * on both ends — the worker on the pushed envelope, the headed box on the reply.
138
+ * Both writes take their file lock asynchronously: this runs on the daemon tick
139
+ * once per peer, and a `sleepSync` lock there freezes every other service.
140
+ */
141
+ export declare function applyPeerFleetState(state: FleetSharedDeviceState, options?: ApplyPeerStateOptions): Promise<ApplyPeerStateResult>;
142
+ /** The remote command one exchange runs on a peer, in the peer's shell dialect. */
143
+ export declare function buildFleetStateExchangeCommand(os?: string): Promise<string>;
144
+ /** The SSH boundary: run `remoteCmd` on `peer` with `input` on stdin. Injected by tests. */
145
+ export type FleetStateDial = (peer: DeviceProfile, remoteCmd: string, input: string) => Promise<SshExecResult>;
146
+ interface ExchangeOptions {
147
+ peers?: DeviceProfile[];
148
+ dial?: FleetStateDial;
149
+ payload?: FleetStateExchangePayload;
150
+ device?: string;
151
+ userAgentsDir?: string;
152
+ cachePath?: string;
153
+ timeoutMs?: number;
154
+ }
155
+ export interface PeerExchangeOutcome {
156
+ device: string;
157
+ delivered: boolean;
158
+ /** Set when the peer's reply was stored — PR 5's per-peer freshness input. */
159
+ receivedAt: number | null;
160
+ merged: number;
161
+ /** Why this peer got nothing this tick (timeout, old CLI, mismatched name, …). */
162
+ error: string | null;
163
+ /** The peer's own publisher failures, verbatim from its reply. */
164
+ peerErrors: string[];
165
+ }
166
+ export interface ExchangeResult {
64
167
  skipped: string | null;
65
- errors: Array<{
66
- device: string;
67
- message: string;
68
- }>;
168
+ outcomes: PeerExchangeOutcome[];
69
169
  }
70
170
  /**
71
- * Read headed publishers from the local user-repo checkout and merge one
72
- * newest-wins batch into the worker cache. No network or subprocess is touched.
171
+ * The headed publisher's fan-out: dial every peer in parallel, each under its
172
+ * own timeout, and apply each reply as it arrives. Never throws for a peer — a
173
+ * timeout, an old CLI with no `--reply`, or a reply naming the wrong device is
174
+ * that peer's `error` and the others are unaffected. A non-headed device skips:
175
+ * workers never initiate (they only answer), so a worker's tick is a no-op here.
73
176
  */
74
- export declare function consumeUsageSnapshotsFromSharedStore(options?: ConsumeUsageSnapshotsOptions): ConsumeUsageSnapshotsResult;
177
+ export declare function exchangeFleetStateWithPeers(options?: ExchangeOptions): Promise<ExchangeResult>;
75
178
  export {};