@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.
- package/CHANGELOG.md +235 -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.js +53 -1
- 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.js +9 -9
- 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/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/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 +5 -10
- 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
|
@@ -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 {};
|
|
@@ -1,21 +1,38 @@
|
|
|
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
|
-
import { isHeadedDeviceRole,
|
|
11
|
-
import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
|
|
23
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
|
|
24
|
+
import { FLEET_SHARED_STATE_VERSION, parseFleetSharedDeviceStateEnvelope, readFleetSharedDeviceStates, readOwnFleetSharedDeviceState, storePeerFleetSharedDeviceState, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
|
|
12
25
|
import { getUserAgentsDir } from '../state.js';
|
|
13
26
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
14
27
|
import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
|
|
15
|
-
/** Cadence of the usage-sync
|
|
28
|
+
/** Cadence of the usage-sync tick — the trust window for a `sync` snapshot. */
|
|
16
29
|
export const USAGE_SYNC_INTERVAL_MS = 15 * 60_000;
|
|
17
30
|
/** Re-publish an unchanged windowed row at least this often so workers' 15-min trust does not lapse. */
|
|
18
31
|
export const USAGE_PUBLISH_HEARTBEAT_MS = 10 * 60_000;
|
|
32
|
+
/** Per-peer SSH budget for one exchange; a peer past it is skipped this tick. */
|
|
33
|
+
export const USAGE_EXCHANGE_PEER_TIMEOUT_MS = 20_000;
|
|
34
|
+
/** Marks the start of the JSON reply on the peer's stdout, so login-shell noise before it is ignored. */
|
|
35
|
+
export const FLEET_STATE_REPLY_MARKER = '@@AGENTS_FLEET_STATE@@';
|
|
19
36
|
function usageMeterSignature(row) {
|
|
20
37
|
return JSON.stringify({
|
|
21
38
|
windows: row.windows,
|
|
@@ -28,7 +45,7 @@ function usageMeterSignature(row) {
|
|
|
28
45
|
/**
|
|
29
46
|
* Keep the previously published `capturedAt` when meters have not moved and
|
|
30
47
|
* the last publish is still inside the heartbeat. Stops statusline re-renders
|
|
31
|
-
* from
|
|
48
|
+
* from rewriting the own-state file every few seconds.
|
|
32
49
|
*/
|
|
33
50
|
export function mergeUsageRowsForPublish(previous, next, nowMs = Date.now()) {
|
|
34
51
|
if (!previous)
|
|
@@ -47,7 +64,7 @@ export function mergeUsageRowsForPublish(previous, next, nowMs = Date.now()) {
|
|
|
47
64
|
}
|
|
48
65
|
return out;
|
|
49
66
|
}
|
|
50
|
-
/** Publish this headed device's stable usage snapshot into its
|
|
67
|
+
/** Publish this headed device's stable usage snapshot into its own state file. */
|
|
51
68
|
export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
52
69
|
const result = {
|
|
53
70
|
published: false,
|
|
@@ -84,68 +101,212 @@ export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
|
84
101
|
return result;
|
|
85
102
|
}
|
|
86
103
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
104
|
+
* Refresh every field this device owns in its own state file: the usage
|
|
105
|
+
* snapshot (headed only), the session digests, and the reserved-auth readiness
|
|
106
|
+
* verdict. Run before the envelope is sent (the headed tick) or printed back
|
|
107
|
+
* (`__usage-ingest --reply`) so a peer always receives current fields. Each
|
|
108
|
+
* publisher is independent: one failing is reported in `errors` and the others
|
|
109
|
+
* still land. All three writers are the async, non-blocking variants
|
|
110
|
+
* (`updateFleetSharedDeviceStateAsync` underneath).
|
|
90
111
|
*/
|
|
91
|
-
export async function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
112
|
+
export async function publishOwnFleetState(options = {}) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const usage = await publishUsageSnapshotToSharedStore(options);
|
|
115
|
+
if (usage.error)
|
|
116
|
+
errors.push(`usage: ${usage.error}`);
|
|
117
|
+
let mirror = { changed: false, count: 0, error: null };
|
|
118
|
+
try {
|
|
119
|
+
const { publishSessionMirrorToSharedStore } = await import('../session/mirror.js');
|
|
120
|
+
const published = await publishSessionMirrorToSharedStore({ userAgentsDir: options.userAgentsDir, device: options.device });
|
|
121
|
+
mirror = { changed: published.changed, count: published.count, error: published.error };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
mirror = { changed: false, count: 0, error: err.message };
|
|
125
|
+
}
|
|
126
|
+
if (mirror.error)
|
|
127
|
+
errors.push(`sessions: ${mirror.error}`);
|
|
128
|
+
let auth = { status: 'unknown', error: null };
|
|
129
|
+
try {
|
|
130
|
+
const { publishReservedAuthVerdict } = await import('../secrets-policy.js');
|
|
131
|
+
const verdict = await publishReservedAuthVerdict({ userAgentsDir: options.userAgentsDir, localName: options.device });
|
|
132
|
+
auth = { status: verdict.status, error: verdict.error };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
auth = { status: 'unknown', error: err.message };
|
|
136
|
+
}
|
|
137
|
+
if (auth.error)
|
|
138
|
+
errors.push(`auth: ${auth.error}`);
|
|
139
|
+
return { usage, mirror, auth, errors };
|
|
140
|
+
}
|
|
141
|
+
/** This device's exchange envelope, from its own state file (or usage-only from the cache). */
|
|
142
|
+
export function buildFleetStatePayload(options = {}) {
|
|
143
|
+
const device = options.device ?? machineId();
|
|
144
|
+
if (options.usageOnly) {
|
|
145
|
+
const state = { version: FLEET_SHARED_STATE_VERSION, device };
|
|
146
|
+
if (isHeadedDeviceRole(options.role ?? selfConfiguredDeviceRole())) {
|
|
147
|
+
const rows = exportClaudeUsageCacheRows(options.cachePath);
|
|
148
|
+
if (Object.keys(rows).length > 0)
|
|
149
|
+
state.usage = { rows };
|
|
150
|
+
}
|
|
151
|
+
return { v: 2, state };
|
|
152
|
+
}
|
|
153
|
+
const state = readOwnFleetSharedDeviceState(device, options.userAgentsDir);
|
|
154
|
+
return { v: 2, state, ...(options.errors?.length ? { errors: options.errors } : {}) };
|
|
103
155
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const ms = Date.parse(row.capturedAt);
|
|
108
|
-
return Number.isFinite(ms) ? ms : null;
|
|
156
|
+
/** Serialize the reply as the peer prints it: marker line, then one JSON line. */
|
|
157
|
+
export function formatFleetStateReply(payload) {
|
|
158
|
+
return `${FLEET_STATE_REPLY_MARKER}\n${JSON.stringify(payload)}\n`;
|
|
109
159
|
}
|
|
110
160
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
161
|
+
* Parse what a peer sent — a v2 exchange envelope, or the legacy v1 bare-rows
|
|
162
|
+
* envelope from an older headed peer. Throws a clear message on anything else;
|
|
163
|
+
* the caller decides whether that is exit code 2 (the receiver) or a per-peer
|
|
164
|
+
* error (the headed tick).
|
|
113
165
|
*/
|
|
114
|
-
export function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return result;
|
|
166
|
+
export function parseFleetStateExchangeInput(raw) {
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(raw);
|
|
119
170
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error('malformed JSON payload');
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
175
|
+
throw new Error('unrecognized usage-sync payload shape');
|
|
176
|
+
}
|
|
177
|
+
const record = parsed;
|
|
178
|
+
if (record.v === 2) {
|
|
179
|
+
const state = parseFleetSharedDeviceStateEnvelope(record.state);
|
|
180
|
+
const errors = Array.isArray(record.errors) ? record.errors.filter((e) => typeof e === 'string') : undefined;
|
|
181
|
+
return { v: 2, state, ...(errors?.length ? { errors } : {}) };
|
|
182
|
+
}
|
|
183
|
+
if (record.v === 1) {
|
|
184
|
+
const rows = record.rows;
|
|
185
|
+
// `typeof [] === 'object'` — an array is NOT a rows map.
|
|
186
|
+
if (typeof rows !== 'object' || rows === null || Array.isArray(rows)) {
|
|
187
|
+
throw new Error('unrecognized usage-sync payload shape');
|
|
188
|
+
}
|
|
189
|
+
return { v: 1, rows: rows };
|
|
190
|
+
}
|
|
191
|
+
throw new Error('unrecognized usage-sync payload shape');
|
|
192
|
+
}
|
|
193
|
+
/** Extract the reply envelope from a peer's stdout (text before the marker is login-shell noise). */
|
|
194
|
+
export function parseFleetStateReply(stdout) {
|
|
195
|
+
const idx = stdout.lastIndexOf(FLEET_STATE_REPLY_MARKER);
|
|
196
|
+
if (idx === -1) {
|
|
197
|
+
throw new Error(stdout.trim()
|
|
198
|
+
? 'no reply envelope on stdout (peer agents-cli predates `__usage-ingest --reply`?)'
|
|
199
|
+
: 'empty reply (peer agents-cli predates `__usage-ingest --reply`?)');
|
|
200
|
+
}
|
|
201
|
+
const body = stdout.slice(idx + FLEET_STATE_REPLY_MARKER.length).trim();
|
|
202
|
+
const payload = parseFleetStateExchangeInput(body);
|
|
203
|
+
if (payload.v !== 2)
|
|
204
|
+
throw new Error('reply is not a v2 exchange envelope');
|
|
205
|
+
return payload;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Rows as the receiver stores them: provenance `sync` (so the trust window is
|
|
209
|
+
* `USAGE_SYNC_TRUST_MS`, not the local-capture bar) and the poller pinned to the
|
|
210
|
+
* device that actually read the endpoint, defaulting to the sender.
|
|
211
|
+
*/
|
|
212
|
+
function stampSyncRows(rows, sender) {
|
|
213
|
+
const out = {};
|
|
214
|
+
for (const [identity, incoming] of Object.entries(rows)) {
|
|
215
|
+
if (!incoming || !Array.isArray(incoming.windows) || incoming.windows.length === 0)
|
|
127
216
|
continue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
217
|
+
out[identity] = { ...incoming, freshnessSource: 'sync', pollerDevice: incoming.pollerDevice ?? sender };
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Take a peer's envelope in: store it as that peer's file (stamped `receivedAt`)
|
|
223
|
+
* and merge its usage rows into this box's cache newest-wins. The same step runs
|
|
224
|
+
* on both ends — the worker on the pushed envelope, the headed box on the reply.
|
|
225
|
+
* Both writes take their file lock asynchronously: this runs on the daemon tick
|
|
226
|
+
* once per peer, and a `sleepSync` lock there freezes every other service.
|
|
227
|
+
*/
|
|
228
|
+
export async function applyPeerFleetState(state, options = {}) {
|
|
229
|
+
const self = normalizeHost(options.device ?? machineId());
|
|
230
|
+
if (normalizeHost(state.device) === self) {
|
|
231
|
+
throw new Error(`peer envelope names this device (${state.device}); refusing to overwrite the own state file`);
|
|
232
|
+
}
|
|
233
|
+
const receivedAt = options.receivedAt ?? Date.now();
|
|
234
|
+
const write = await storePeerFleetSharedDeviceState(state, options.userAgentsDir ?? getUserAgentsDir(), receivedAt);
|
|
235
|
+
const merged = state.usage
|
|
236
|
+
? await ingestPeerClaudeUsageRows(stampSyncRows(state.usage.rows, state.device), options.cachePath)
|
|
237
|
+
: 0;
|
|
238
|
+
return { path: write.path, merged, receivedAt };
|
|
239
|
+
}
|
|
240
|
+
/** The remote command one exchange runs on a peer, in the peer's shell dialect. */
|
|
241
|
+
export async function buildFleetStateExchangeCommand(os) {
|
|
242
|
+
const { buildRemoteAgentsInvocation, buildWindowsStdinAgentsCommand, remoteShellFor } = await import('../hosts/remote-cmd.js');
|
|
243
|
+
// The Windows `agents.ps1` shim does not forward ssh-piped stdin, so the
|
|
244
|
+
// payload goes through a temp file and `--from <path>` there.
|
|
245
|
+
return remoteShellFor(os) === 'powershell'
|
|
246
|
+
? buildWindowsStdinAgentsCommand(['__usage-ingest', '--reply'])
|
|
247
|
+
: buildRemoteAgentsInvocation(['__usage-ingest', '--reply'], undefined, os);
|
|
248
|
+
}
|
|
249
|
+
async function sshDial(timeoutMs) {
|
|
250
|
+
const { sshExecAsync } = await import('../ssh-exec.js');
|
|
251
|
+
const { sshTargetFor, deviceIdentityArgs } = await import('../devices/connect.js');
|
|
252
|
+
return (peer, remoteCmd, input) => sshExecAsync(sshTargetFor(peer), remoteCmd, {
|
|
253
|
+
input,
|
|
254
|
+
timeoutMs,
|
|
255
|
+
extraSshArgs: deviceIdentityArgs(peer),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function defaultPeers(self) {
|
|
259
|
+
const { isDialableDevice, loadDevicesSync } = await import('../devices/registry.js');
|
|
260
|
+
return Object.values(loadDevicesSync())
|
|
261
|
+
.filter((d) => normalizeHost(d.name) !== self && isDialableDevice(d))
|
|
262
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The headed publisher's fan-out: dial every peer in parallel, each under its
|
|
266
|
+
* own timeout, and apply each reply as it arrives. Never throws for a peer — a
|
|
267
|
+
* timeout, an old CLI with no `--reply`, or a reply naming the wrong device is
|
|
268
|
+
* that peer's `error` and the others are unaffected. A non-headed device skips:
|
|
269
|
+
* workers never initiate (they only answer), so a worker's tick is a no-op here.
|
|
270
|
+
*/
|
|
271
|
+
export async function exchangeFleetStateWithPeers(options = {}) {
|
|
272
|
+
const device = options.device ?? machineId();
|
|
273
|
+
const self = normalizeHost(device);
|
|
274
|
+
const peers = options.peers ?? await defaultPeers(self);
|
|
275
|
+
if (peers.length === 0)
|
|
276
|
+
return { skipped: 'no dialable peer in the device registry', outcomes: [] };
|
|
277
|
+
const timeoutMs = options.timeoutMs ?? USAGE_EXCHANGE_PEER_TIMEOUT_MS;
|
|
278
|
+
const dial = options.dial ?? await sshDial(timeoutMs);
|
|
279
|
+
const payload = options.payload ?? buildFleetStatePayload({ device, userAgentsDir: options.userAgentsDir });
|
|
280
|
+
const input = JSON.stringify(payload);
|
|
281
|
+
const outcomes = await Promise.all(peers.map(async (peer) => {
|
|
282
|
+
const outcome = { device: peer.name, delivered: false, receivedAt: null, merged: 0, error: null, peerErrors: [] };
|
|
283
|
+
try {
|
|
284
|
+
const remoteCmd = await buildFleetStateExchangeCommand(peer.shell === 'powershell' ? 'windows' : undefined);
|
|
285
|
+
const res = await dial(peer, remoteCmd, input);
|
|
286
|
+
if (res.timedOut)
|
|
287
|
+
throw new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
288
|
+
if (res.code !== 0) {
|
|
289
|
+
const detail = res.stderr.trim().split('\n').pop() ?? '';
|
|
290
|
+
throw new Error(`exited ${res.code ?? 'without a status'}${detail ? `: ${detail}` : ''}`);
|
|
291
|
+
}
|
|
292
|
+
const reply = parseFleetStateReply(res.stdout);
|
|
293
|
+
if (normalizeHost(reply.state.device) !== normalizeHost(peer.name)) {
|
|
294
|
+
throw new Error(`reply names device '${reply.state.device}', expected '${peer.name}'`);
|
|
141
295
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
296
|
+
const applied = await applyPeerFleetState(reply.state, {
|
|
297
|
+
device,
|
|
298
|
+
userAgentsDir: options.userAgentsDir,
|
|
299
|
+
cachePath: options.cachePath,
|
|
300
|
+
});
|
|
301
|
+
outcome.delivered = true;
|
|
302
|
+
outcome.receivedAt = applied.receivedAt;
|
|
303
|
+
outcome.merged = applied.merged;
|
|
304
|
+
outcome.peerErrors = reply.errors ?? [];
|
|
146
305
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
306
|
+
catch (err) {
|
|
307
|
+
outcome.error = err.message;
|
|
308
|
+
}
|
|
309
|
+
return outcome;
|
|
310
|
+
}));
|
|
311
|
+
return { skipped: null, outcomes };
|
|
151
312
|
}
|
|
@@ -494,6 +494,17 @@ export interface FormatUsageSummaryOpts {
|
|
|
494
494
|
export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined, version?: string): FormatUsageSummaryOpts;
|
|
495
495
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
496
496
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: FormatUsageSummaryOpts): string;
|
|
497
|
+
/**
|
|
498
|
+
* The snapshot's windows that are still LIVE — reset time not yet passed. A
|
|
499
|
+
* window past its `resetsAt` has rolled over: its `usedPercent` is a reading of
|
|
500
|
+
* the PREVIOUS period, not a live throttle (PHNX-4116). Both the throttle
|
|
501
|
+
* verdict ({@link deriveUsageStatusFromSnapshot}) and the displayed
|
|
502
|
+
* `usedPercent` (`devices/harness-inventory.ts`) MUST select from this SAME set,
|
|
503
|
+
* or a maxed-then-reset account reads `available` yet shows a stale 99% next to
|
|
504
|
+
* its "ready" verdict (#3705). Callers still narrow further (dropping
|
|
505
|
+
* `sonnet_week`, the model sub-limit); this is only the reset-rollover cut.
|
|
506
|
+
*/
|
|
507
|
+
export declare function liveUsageWindows(snapshot: UsageSnapshot, now?: number): UsageWindow[];
|
|
497
508
|
/**
|
|
498
509
|
* Derive an account's real throttle state from its live usage windows — the
|
|
499
510
|
* single signal both the `agents view` badge and run-rotation eligibility share
|
|
@@ -513,7 +524,7 @@ export declare function formatUsageSummary(plan: string | null, snapshot: UsageS
|
|
|
513
524
|
* The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
|
|
514
525
|
* one model, not the account, so it shouldn't flip the whole row to throttled.
|
|
515
526
|
*/
|
|
516
|
-
export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined): 'available' | 'rate_limited' | null;
|
|
527
|
+
export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined, now?: number): 'available' | 'rate_limited' | null;
|
|
517
528
|
/** A prior sample of one window's utilization, for burn-rate projection. */
|
|
518
529
|
interface UsagePriorSample {
|
|
519
530
|
/** Epoch ms the prior snapshot was captured. */
|
|
@@ -768,7 +779,9 @@ export declare function exportClaudeUsageCacheRows(cachePath?: string): Record<s
|
|
|
768
779
|
* a fresher row another peer (or, on a headed receiver, the local status-line)
|
|
769
780
|
* already wrote. An incoming row with no `capturedAt` cannot prove it is newer, so
|
|
770
781
|
* it never displaces an existing timestamped row. Returns the count updated.
|
|
771
|
-
* Locked + atomic like every other cache writer
|
|
782
|
+
* Locked + atomic like every other cache writer, but through the ASYNC lock: it
|
|
783
|
+
* runs on the daemon's usage-sync tick once per peer reply (PHNX-4116), where
|
|
784
|
+
* the sync lock's `sleepSync` would freeze every other service under contention.
|
|
772
785
|
*
|
|
773
786
|
* Deliberately NOT role-gated on the receiver. "Consume only on worker/unmarked"
|
|
774
787
|
* is a SENDER-side optimization (don't waste a push on a headed peer that reads
|
|
@@ -778,7 +791,7 @@ export declare function exportClaudeUsageCacheRows(cachePath?: string): Record<s
|
|
|
778
791
|
* runs now shows a usage bar), so gating here on the receiver's own — laggier —
|
|
779
792
|
* view of its role would only reject legitimate data.
|
|
780
793
|
*/
|
|
781
|
-
export declare function ingestPeerClaudeUsageRows(rows: Record<string, CachedUsageSnapshot>, cachePath?: string): number
|
|
794
|
+
export declare function ingestPeerClaudeUsageRows(rows: Record<string, CachedUsageSnapshot>, cachePath?: string): Promise<number>;
|
|
782
795
|
/**
|
|
783
796
|
* Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
|
|
784
797
|
* spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
|