@phnx-labs/agents-cli 1.22.116 → 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.
- package/CHANGELOG.md +247 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +104 -12
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup-term.d.ts +4 -3
- package/dist/commands/setup-term.js +5 -4
- package/dist/commands/setup.js +11 -11
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +8 -12
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -212,9 +212,18 @@ export function hasStaleUsage(candidate, nowMs = Date.now()) {
|
|
|
212
212
|
const capturedAt = snapshot?.capturedAt;
|
|
213
213
|
if (!capturedAt || !snapshot?.windows.length)
|
|
214
214
|
return false;
|
|
215
|
+
// A row that arrived via sync (D1, PHNX-4116) cannot be refreshed on THIS box —
|
|
216
|
+
// the account's poller lives on the headed device that published it, not here.
|
|
217
|
+
// Its age is shown, never used to refuse: a blind synced pool takes a
|
|
218
|
+
// floor-weight pick (capacityWeight → UNVERIFIED_WEIGHT) and
|
|
219
|
+
// rotationFailoverChain handles a real 429. Only a locally-captured reading a
|
|
220
|
+
// broken poller left hours old is genuinely "stale" for a refusal. See the
|
|
221
|
+
// GWT-E5d amendment.
|
|
222
|
+
if (snapshot.freshness?.source === 'sync')
|
|
223
|
+
return false;
|
|
215
224
|
return nowMs - capturedAt.getTime() > USAGE_STALE_REFUSAL_MAX_AGE_MS;
|
|
216
225
|
}
|
|
217
|
-
function hasUsageAvailable(candidate) {
|
|
226
|
+
function hasUsageAvailable(candidate, now = Date.now()) {
|
|
218
227
|
const snapshot = candidate.usageSnapshot;
|
|
219
228
|
if (snapshot) {
|
|
220
229
|
// Eligibility mirrors the `agents view` throttle badge exactly
|
|
@@ -225,7 +234,7 @@ function hasUsageAvailable(candidate) {
|
|
|
225
234
|
// stayed "eligible" and the router kept launching into it while `ag view`
|
|
226
235
|
// showed it rate-limited. Capacity *weighting* still ranks eligible accounts
|
|
227
236
|
// by weekly headroom; this gate only decides can-it-run-right-now.
|
|
228
|
-
const status = deriveUsageStatusFromSnapshot(snapshot);
|
|
237
|
+
const status = deriveUsageStatusFromSnapshot(snapshot, now);
|
|
229
238
|
if (status !== null)
|
|
230
239
|
return status !== 'rate_limited';
|
|
231
240
|
}
|
|
@@ -279,11 +288,11 @@ export function readinessFromCandidate(candidate, now = Date.now(), model) {
|
|
|
279
288
|
return { ready: false, reason: 'model_limited', email: candidate.email };
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
|
-
if (hasUsageAvailable(candidate)) {
|
|
291
|
+
if (hasUsageAvailable(candidate, now)) {
|
|
283
292
|
return { ready: true };
|
|
284
293
|
}
|
|
285
294
|
const snap = candidate.usageSnapshot;
|
|
286
|
-
const snapRateLimited = !!snap && snap.windows.length > 0 && deriveUsageStatusFromSnapshot(snap) === 'rate_limited';
|
|
295
|
+
const snapRateLimited = !!snap && snap.windows.length > 0 && deriveUsageStatusFromSnapshot(snap, now) === 'rate_limited';
|
|
287
296
|
const reason = !snapRateLimited && candidate.usageStatus === 'out_of_credits' ? 'out_of_credits' : 'rate_limited';
|
|
288
297
|
return { ready: false, reason, email: candidate.email };
|
|
289
298
|
}
|
|
@@ -573,11 +582,11 @@ export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Dat
|
|
|
573
582
|
export function classifyHarnessCandidates(byHarness, nowMs = Date.now()) {
|
|
574
583
|
const summaries = [];
|
|
575
584
|
for (const [agent, candidates] of byHarness) {
|
|
576
|
-
const eligible = candidates.filter((c) => isRotationEligible(c));
|
|
585
|
+
const eligible = candidates.filter((c) => isRotationEligible(c, nowMs));
|
|
577
586
|
if (eligible.length === 0) {
|
|
578
587
|
const counts = new Map();
|
|
579
588
|
for (const c of candidates) {
|
|
580
|
-
const readiness = readinessFromCandidate(c);
|
|
589
|
+
const readiness = readinessFromCandidate(c, nowMs);
|
|
581
590
|
const reason = readiness.ready ? 'ineligible' : readiness.reason;
|
|
582
591
|
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
583
592
|
}
|
|
@@ -668,7 +677,7 @@ export function formatNoHealthyAccountError(agent, strategy, excluded, nowMs = D
|
|
|
668
677
|
const excludedStr = excluded.length === 0
|
|
669
678
|
? 'no installed versions'
|
|
670
679
|
: excluded.map((c) => {
|
|
671
|
-
const readiness = readinessFromCandidate(c);
|
|
680
|
+
const readiness = readinessFromCandidate(c, nowMs);
|
|
672
681
|
const reason = readiness.ready ? 'ineligible' : readiness.reason;
|
|
673
682
|
return `${c.version} (${reason})`;
|
|
674
683
|
}).join(', ');
|
|
@@ -700,7 +709,12 @@ export function formatNoVerifiedUsageError(agent, strategy, candidates, nowMs =
|
|
|
700
709
|
? 'no signed-in accounts'
|
|
701
710
|
: candidates.map((c) => {
|
|
702
711
|
const age = snapshotAgeMinutes(c, nowMs);
|
|
703
|
-
|
|
712
|
+
// A synced row's age never drives a refusal (see hasStaleUsage), so name
|
|
713
|
+
// it as synced rather than letting the operator read it as the culprit.
|
|
714
|
+
const synced = c.usageSnapshot?.freshness?.source === 'sync';
|
|
715
|
+
const staleness = age === null
|
|
716
|
+
? 'no usage snapshot'
|
|
717
|
+
: `usage ${age}m old${synced ? ', synced' : ''}`;
|
|
704
718
|
return `${c.version} (${staleness})`;
|
|
705
719
|
}).join(', ');
|
|
706
720
|
const maxAgeMin = Math.round(USAGE_STALE_REFUSAL_MAX_AGE_MS / 60_000);
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** Stdin grew past {@link REMOTE_STDOUT_MAX_BYTES}; the payload was refused unread. */
|
|
2
|
+
export declare class UsageIngestInputTooLargeError extends Error {
|
|
3
|
+
readonly limitBytes: number;
|
|
4
|
+
constructor(limitBytes: number);
|
|
5
|
+
}
|
|
6
|
+
export declare function runUsageIngest(argv?: string[]): Promise<number>;
|
|
@@ -1,35 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `agents __usage-ingest` receiver
|
|
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
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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', () =>
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
126
|
+
receivedAt?: number;
|
|
60
127
|
}
|
|
61
|
-
interface
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
device: string;
|
|
67
|
-
message: string;
|
|
68
|
-
}>;
|
|
168
|
+
outcomes: PeerExchangeOutcome[];
|
|
69
169
|
}
|
|
70
170
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
177
|
+
export declare function exchangeFleetStateWithPeers(options?: ExchangeOptions): Promise<ExchangeResult>;
|
|
75
178
|
export {};
|