@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -1,107 +1,52 @@
|
|
|
1
|
-
import { type DeviceProfile } from '../devices/registry.js';
|
|
2
|
-
import { type ConfiguredDeviceRole } from '../device-config.js';
|
|
3
|
-
import { type CachedUsageSnapshot } from './usage.js';
|
|
4
|
-
/** How long a single peer push may take before it is abandoned for this tick. */
|
|
5
|
-
export declare const USAGE_PUSH_DEADLINE_MS = 20000;
|
|
6
|
-
export declare const USAGE_PULL_DEADLINE_MS = 20000;
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
2
|
+
* Fleet distribution of identity-keyed Claude usage through the user repo.
|
|
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.
|
|
14
9
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
import { type ConfiguredDeviceRole } from '../device-config.js';
|
|
11
|
+
import { type CachedUsageSnapshot } from './usage.js';
|
|
12
|
+
/** Legacy hidden ingest/export envelope kept for older fleet CLI compatibility. */
|
|
17
13
|
export interface UsageSyncPayload {
|
|
18
14
|
v: 1;
|
|
19
15
|
rows: Record<string, CachedUsageSnapshot>;
|
|
20
16
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/** Live-ish reachability from the tailscale snapshot; offline peers are skipped. */
|
|
27
|
-
online: boolean;
|
|
28
|
-
/** Managed known-hosts pin — an unpinned host is skipped, never TOFU-accepted. */
|
|
29
|
-
pinned: boolean;
|
|
17
|
+
export interface PublishUsageSnapshotOptions {
|
|
18
|
+
userAgentsDir?: string;
|
|
19
|
+
cachePath?: string;
|
|
20
|
+
role?: ConfiguredDeviceRole;
|
|
21
|
+
device?: string;
|
|
30
22
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reason: string;
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* Decide, per peer, whether to push the local usage snapshot. Pure.
|
|
41
|
-
*
|
|
42
|
-
* - `selfIsPublisher` is false on a `worker`/unmarked box: it has no authoritative
|
|
43
|
-
* usage to publish, so every peer is skipped.
|
|
44
|
-
* - `hasLocalRows` false means the local cache is empty (nothing to teach yet).
|
|
45
|
-
* - A HEADED peer is skipped — it reads its own usage; pushing risks nothing
|
|
46
|
-
* (the merge is newest-wins) but is wasted work, and keeping the fan-out to
|
|
47
|
-
* consumers only makes the intent legible.
|
|
48
|
-
*/
|
|
49
|
-
export declare function planUsagePush(selfIsPublisher: boolean, hasLocalRows: boolean, targets: UsagePushTarget[]): UsageSyncPlanItem[];
|
|
50
|
-
export interface UsageSyncResult {
|
|
51
|
-
pushed: string[];
|
|
52
|
-
skipped: Array<{
|
|
53
|
-
device: string;
|
|
54
|
-
reason: string;
|
|
55
|
-
}>;
|
|
56
|
-
errors: Array<{
|
|
57
|
-
device: string;
|
|
58
|
-
message: string;
|
|
59
|
-
}>;
|
|
23
|
+
export interface PublishUsageSnapshotResult {
|
|
24
|
+
published: boolean;
|
|
25
|
+
changed: boolean;
|
|
26
|
+
skipped: string | null;
|
|
27
|
+
error: string | null;
|
|
28
|
+
path: string | null;
|
|
60
29
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/** Deliver the serialized payload to one peer. Default: ssh `__usage-ingest`. */
|
|
70
|
-
push?: (device: DeviceProfile, payload: string) => {
|
|
71
|
-
ok: boolean;
|
|
72
|
-
message?: string;
|
|
73
|
-
};
|
|
30
|
+
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
31
|
+
export declare function publishUsageSnapshotToSharedStore(options?: PublishUsageSnapshotOptions): PublishUsageSnapshotResult;
|
|
32
|
+
export interface ConsumeUsageSnapshotsOptions {
|
|
33
|
+
userAgentsDir?: string;
|
|
34
|
+
cachePath?: string;
|
|
35
|
+
role?: ConfiguredDeviceRole;
|
|
36
|
+
device?: string;
|
|
37
|
+
roles?: Record<string, ConfiguredDeviceRole>;
|
|
74
38
|
}
|
|
75
|
-
export interface
|
|
76
|
-
|
|
39
|
+
export interface ConsumeUsageSnapshotsResult {
|
|
40
|
+
sources: string[];
|
|
77
41
|
merged: number;
|
|
78
42
|
skipped: string | null;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
listDevices?: () => DeviceProfile[];
|
|
84
|
-
listRoles?: () => Record<string, ConfiguredDeviceRole>;
|
|
85
|
-
isPinned?: (name: string) => boolean;
|
|
86
|
-
exportRows?: () => Record<string, CachedUsageSnapshot>;
|
|
87
|
-
ingestRows?: (rows: Record<string, CachedUsageSnapshot>) => number;
|
|
88
|
-
/** Injectable clock for the staleness gate; defaults to `Date.now`. */
|
|
89
|
-
now?: () => number;
|
|
90
|
-
/** Read the versioned payload from the primary. Default: ssh `__usage-export`. */
|
|
91
|
-
pull?: (device: DeviceProfile) => {
|
|
92
|
-
ok: boolean;
|
|
93
|
-
stdout?: string;
|
|
94
|
-
message?: string;
|
|
95
|
-
};
|
|
43
|
+
errors: Array<{
|
|
44
|
+
device: string;
|
|
45
|
+
message: string;
|
|
46
|
+
}>;
|
|
96
47
|
}
|
|
97
48
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* a `desktop` is used only when the fleet has no personal device.
|
|
101
|
-
*/
|
|
102
|
-
export declare function pullUsageFromPrimary(deps?: UsagePullDeps): UsagePullResult;
|
|
103
|
-
/**
|
|
104
|
-
* Push the local identity-keyed usage snapshot to every reachable, pinned,
|
|
105
|
-
* non-headed peer. A no-op on a non-headed box or when the local cache is empty.
|
|
49
|
+
* Read headed publishers from the local user-repo checkout and merge one
|
|
50
|
+
* newest-wins batch into the worker cache. No network or subprocess is touched.
|
|
106
51
|
*/
|
|
107
|
-
export declare function
|
|
52
|
+
export declare function consumeUsageSnapshotsFromSharedStore(options?: ConsumeUsageSnapshotsOptions): ConsumeUsageSnapshotsResult;
|
|
@@ -1,231 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fleet
|
|
2
|
+
* Fleet distribution of identity-keyed Claude usage through the user repo.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `user:inference` setup-token, which the usage endpoint 403s (RUSH-2392), so its
|
|
10
|
-
* local `claude-usage.json` stays blank and `agents view` shows no S:/W: bars.
|
|
11
|
-
*
|
|
12
|
-
* This closes that gap the same way {@link ../secrets/reserved-sync.ts} closes
|
|
13
|
-
* the auth-token gap: each headed daemon PUSHES its identity-keyed usage rows to
|
|
14
|
-
* the worker peers that cannot read them, which merge NEWEST-WINS
|
|
15
|
-
* ({@link ingestPeerClaudeUsageRows}). Publish direction is role-driven —
|
|
16
|
-
* personal/desktop publish, worker/unmarked consume — so it composes with the
|
|
17
|
-
* device-role taxonomy rather than adding a second notion of "which box is real".
|
|
18
|
-
*
|
|
19
|
-
* Double-fire safety: each daemon only writes the DESTINATION's own cache, and
|
|
20
|
-
* the merge is idempotent + timestamp-guarded, so two headed publishers pushing
|
|
21
|
-
* the same account to one worker converge on the freshest snapshot regardless of
|
|
22
|
-
* order. The input a publisher reads is its OWN cache (device-local state), so an
|
|
23
|
-
* unrestricted per-device fire is correct — no shared queue.
|
|
24
|
-
*
|
|
25
|
-
* The planner is pure so tests cover every skip/push branch with no SSH.
|
|
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.
|
|
26
9
|
*/
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { loadDevicesSync } from '../devices/registry.js';
|
|
31
|
-
import { sshTargetFor } from '../devices/connect.js';
|
|
32
|
-
import { isHostPinned, isDevicePinned } from '../devices/known-hosts.js';
|
|
10
|
+
import { isHeadedDeviceRole, listConfiguredDeviceRoles, selfConfiguredDeviceRole } from '../device-config.js';
|
|
11
|
+
import { readFleetSharedDeviceStates, updateFleetSharedDeviceState, } from '../fleet-shared-state.js';
|
|
12
|
+
import { getUserAgentsDir } from '../state.js';
|
|
33
13
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
34
|
-
import { isHeadedDeviceRole, listConfiguredDeviceRoles, selfConfiguredDeviceRole, } from '../device-config.js';
|
|
35
14
|
import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
|
|
36
|
-
/**
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/** Epoch ms of a cache row's capture time, or null when unparseable/absent. */
|
|
49
|
-
function capturedAtMs(capturedAt) {
|
|
50
|
-
if (!capturedAt)
|
|
51
|
-
return null;
|
|
52
|
-
const ms = Date.parse(capturedAt);
|
|
53
|
-
return Number.isFinite(ms) ? ms : null;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Decide, per peer, whether to push the local usage snapshot. Pure.
|
|
57
|
-
*
|
|
58
|
-
* - `selfIsPublisher` is false on a `worker`/unmarked box: it has no authoritative
|
|
59
|
-
* usage to publish, so every peer is skipped.
|
|
60
|
-
* - `hasLocalRows` false means the local cache is empty (nothing to teach yet).
|
|
61
|
-
* - A HEADED peer is skipped — it reads its own usage; pushing risks nothing
|
|
62
|
-
* (the merge is newest-wins) but is wasted work, and keeping the fan-out to
|
|
63
|
-
* consumers only makes the intent legible.
|
|
64
|
-
*/
|
|
65
|
-
export function planUsagePush(selfIsPublisher, hasLocalRows, targets) {
|
|
66
|
-
if (!selfIsPublisher) {
|
|
67
|
-
return targets.map((d) => ({
|
|
68
|
-
action: 'skip',
|
|
69
|
-
device: d.name,
|
|
70
|
-
reason: 'this device is not a usage publisher (mark it personal or desktop)',
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
if (!hasLocalRows) {
|
|
74
|
-
return targets.map((d) => ({ action: 'skip', device: d.name, reason: 'no local usage snapshot to publish' }));
|
|
75
|
-
}
|
|
76
|
-
return targets.map((d) => {
|
|
77
|
-
if (isHeadedDeviceRole(d.role))
|
|
78
|
-
return { action: 'skip', device: d.name, reason: 'headed peer reads its own usage' };
|
|
79
|
-
if (!d.online)
|
|
80
|
-
return { action: 'skip', device: d.name, reason: 'offline' };
|
|
81
|
-
if (!d.pinned) {
|
|
82
|
-
return { action: 'skip', device: d.name, reason: `host key not pinned; run \`agents ssh ${d.name}\` once` };
|
|
83
|
-
}
|
|
84
|
-
return { action: 'push', device: d.name };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
/** Production push: pipe the payload to the peer's `agents __usage-ingest` over ssh. */
|
|
88
|
-
function defaultUsagePush(device, payload) {
|
|
89
|
-
const target = sshTargetFor(device);
|
|
90
|
-
const os = resolveRemoteOsSync(device.name);
|
|
91
|
-
// A Windows peer's `agents.ps1` shim does not forward ssh-piped stdin, so hand
|
|
92
|
-
// the payload through a temp file (`--from`) instead of stdin — same workaround
|
|
93
|
-
// the secrets push uses. A POSIX peer reads stdin directly.
|
|
94
|
-
const remoteCmd = remoteShellFor(os) === 'powershell'
|
|
95
|
-
? buildWindowsStdinAgentsCommand(['__usage-ingest'])
|
|
96
|
-
: buildRemoteAgentsInvocation(['__usage-ingest'], undefined, os);
|
|
97
|
-
const res = sshExec(target, remoteCmd, { input: payload, timeoutMs: USAGE_PUSH_DEADLINE_MS });
|
|
98
|
-
if (res.timedOut)
|
|
99
|
-
return { ok: false, message: 'timed out' };
|
|
100
|
-
if (res.code !== 0)
|
|
101
|
-
return { ok: false, message: res.stderr.trim() || `remote exit ${res.code}` };
|
|
102
|
-
return { ok: true };
|
|
103
|
-
}
|
|
104
|
-
/** Production pull: read the primary's cache through its hidden export verb. */
|
|
105
|
-
function defaultUsagePull(device) {
|
|
106
|
-
const target = sshTargetFor(device);
|
|
107
|
-
const os = resolveRemoteOsSync(device.name);
|
|
108
|
-
const remoteCmd = buildRemoteAgentsInvocation(['__usage-export'], undefined, os);
|
|
109
|
-
const res = sshExec(target, remoteCmd, { timeoutMs: USAGE_PULL_DEADLINE_MS });
|
|
110
|
-
if (res.timedOut)
|
|
111
|
-
return { ok: false, message: 'timed out' };
|
|
112
|
-
if (res.code !== 0)
|
|
113
|
-
return { ok: false, message: res.stderr.trim() || `remote exit ${res.code}` };
|
|
114
|
-
return { ok: true, stdout: res.stdout };
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Pull usage from the fleet's primary headed device when this worker's local
|
|
118
|
-
* cache is empty or contains an expired row. `personal` is the primary role;
|
|
119
|
-
* a `desktop` is used only when the fleet has no personal device.
|
|
120
|
-
*/
|
|
121
|
-
export function pullUsageFromPrimary(deps = {}) {
|
|
122
|
-
const result = { pulledFrom: null, merged: 0, skipped: null, error: null };
|
|
123
|
-
if ((deps.selfRole ?? selfConfiguredDeviceRole)() !== 'worker') {
|
|
124
|
-
result.skipped = 'this device is not a worker';
|
|
15
|
+
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
16
|
+
export function publishUsageSnapshotToSharedStore(options = {}) {
|
|
17
|
+
const result = {
|
|
18
|
+
published: false,
|
|
19
|
+
changed: false,
|
|
20
|
+
skipped: null,
|
|
21
|
+
error: null,
|
|
22
|
+
path: null,
|
|
23
|
+
};
|
|
24
|
+
const role = options.role ?? selfConfiguredDeviceRole();
|
|
25
|
+
if (!isHeadedDeviceRole(role)) {
|
|
26
|
+
result.skipped = 'this device is not a usage publisher (mark it personal or desktop)';
|
|
125
27
|
return result;
|
|
126
28
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// SAME on-disk cache twice (exportRows() and readRow()) and compared window
|
|
131
|
-
// COUNT, so `fresh.windows.length === row.windows.length` was always true and
|
|
132
|
-
// any non-empty worker cache skipped the pull forever — which is how a worker
|
|
133
|
-
// served a days-old snapshot indefinitely and balanced launched into an
|
|
134
|
-
// account already at its weekly cap. A worker cannot self-read usage, so any
|
|
135
|
-
// row older than the sync cadence is stale and must trigger a pull; the merge
|
|
136
|
-
// is newest-wins + idempotent, so pulling when the primary is no fresher is a
|
|
137
|
-
// harmless no-op.
|
|
138
|
-
const now = (deps.now ?? Date.now)();
|
|
139
|
-
const staleCutoff = now - USAGE_SYNC_MAX_AGE_MS;
|
|
140
|
-
const allFresh = localEntries.length > 0 &&
|
|
141
|
-
localEntries.every(([, row]) => {
|
|
142
|
-
const ms = capturedAtMs(row.capturedAt);
|
|
143
|
-
return ms !== null && ms >= staleCutoff;
|
|
144
|
-
});
|
|
145
|
-
if (allFresh) {
|
|
146
|
-
result.skipped = 'local usage cache is fresh';
|
|
29
|
+
const rows = exportClaudeUsageCacheRows(options.cachePath);
|
|
30
|
+
if (Object.keys(rows).length === 0) {
|
|
31
|
+
result.skipped = 'no local usage snapshot to publish';
|
|
147
32
|
return result;
|
|
148
33
|
}
|
|
149
|
-
const devices = deps.listDevices?.() ?? Object.values(loadDevicesSync());
|
|
150
|
-
const roles = deps.listRoles?.() ?? (deps.listDevices
|
|
151
|
-
? listConfiguredDeviceRoles(devices.map((device) => device.name))
|
|
152
|
-
: listConfiguredDeviceRoles());
|
|
153
|
-
const isPinned = deps.isPinned ?? isHostPinned;
|
|
154
|
-
const primary = devices
|
|
155
|
-
.filter((device) => roles[device.name] === 'personal')
|
|
156
|
-
.concat(devices.filter((device) => roles[device.name] === 'desktop'))
|
|
157
|
-
.find((device) => device.tailscale?.online !== false && isDevicePinned(device, isPinned));
|
|
158
|
-
if (!primary) {
|
|
159
|
-
result.error = 'no reachable, pinned personal or desktop device is configured as the usage primary';
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
const outcome = (deps.pull ?? defaultUsagePull)(primary);
|
|
163
|
-
if (!outcome.ok) {
|
|
164
|
-
result.error = outcome.message ?? 'pull failed';
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
let payload;
|
|
168
34
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// worker's cache stays null, and the PHNX-3392 capacity floor silently
|
|
174
|
-
// becomes the only thing standing between a blind pool and an exhausted pick.
|
|
175
|
-
payload = JSON.parse(stripClixml(outcome.stdout ?? ''));
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
result.error = 'primary returned malformed JSON';
|
|
179
|
-
return result;
|
|
35
|
+
const write = updateFleetSharedDeviceState(options.device ?? machineId(), { usage: { rows } }, options.userAgentsDir ?? getUserAgentsDir());
|
|
36
|
+
result.published = true;
|
|
37
|
+
result.changed = write.changed;
|
|
38
|
+
result.path = write.path;
|
|
180
39
|
}
|
|
181
|
-
|
|
182
|
-
result.error =
|
|
183
|
-
return result;
|
|
40
|
+
catch (err) {
|
|
41
|
+
result.error = err.message;
|
|
184
42
|
}
|
|
185
|
-
result.pulledFrom = primary.name;
|
|
186
|
-
result.merged = (deps.ingestRows ?? ingestPeerClaudeUsageRows)(payload.rows);
|
|
187
43
|
return result;
|
|
188
44
|
}
|
|
45
|
+
function capturedAtMs(row) {
|
|
46
|
+
if (!row.capturedAt)
|
|
47
|
+
return null;
|
|
48
|
+
const ms = Date.parse(row.capturedAt);
|
|
49
|
+
return Number.isFinite(ms) ? ms : null;
|
|
50
|
+
}
|
|
189
51
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
52
|
+
* Read headed publishers from the local user-repo checkout and merge one
|
|
53
|
+
* newest-wins batch into the worker cache. No network or subprocess is touched.
|
|
192
54
|
*/
|
|
193
|
-
export function
|
|
194
|
-
const result = {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
role: roles[d.name],
|
|
207
|
-
online: d.tailscale?.online !== false,
|
|
208
|
-
pinned: isDevicePinned(d, isPinned),
|
|
209
|
-
}));
|
|
210
|
-
const rows = (deps.exportRows ?? exportClaudeUsageCacheRows)();
|
|
211
|
-
const plan = planUsagePush(isHeadedDeviceRole(selfRole), Object.keys(rows).length > 0, targets);
|
|
212
|
-
const payload = JSON.stringify({ v: 1, rows });
|
|
213
|
-
const push = deps.push ?? defaultUsagePush;
|
|
214
|
-
for (const item of plan) {
|
|
215
|
-
if (item.action === 'skip') {
|
|
216
|
-
result.skipped.push({ device: item.device, reason: item.reason });
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const device = byName.get(item.device);
|
|
220
|
-
if (!device) {
|
|
221
|
-
result.errors.push({ device: item.device, message: 'device left the registry mid-sync' });
|
|
55
|
+
export function consumeUsageSnapshotsFromSharedStore(options = {}) {
|
|
56
|
+
const result = { sources: [], merged: 0, skipped: null, errors: [] };
|
|
57
|
+
if ((options.role ?? selfConfiguredDeviceRole()) !== 'worker') {
|
|
58
|
+
result.skipped = 'this device is not a worker';
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
const read = readFleetSharedDeviceStates(options.userAgentsDir ?? getUserAgentsDir());
|
|
62
|
+
result.errors.push(...read.errors);
|
|
63
|
+
const self = normalizeHost(options.device ?? machineId());
|
|
64
|
+
const roles = options.roles ?? listConfiguredDeviceRoles(read.states.map((state) => state.device));
|
|
65
|
+
const rows = {};
|
|
66
|
+
for (const state of read.states) {
|
|
67
|
+
if (normalizeHost(state.device) === self || !isHeadedDeviceRole(roles[state.device]) || !state.usage)
|
|
222
68
|
continue;
|
|
69
|
+
result.sources.push(state.device);
|
|
70
|
+
for (const [identity, incoming] of Object.entries(state.usage.rows)) {
|
|
71
|
+
if (!incoming || !Array.isArray(incoming.windows) || incoming.windows.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
const current = rows[identity];
|
|
74
|
+
if (!current) {
|
|
75
|
+
rows[identity] = incoming;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const incomingMs = capturedAtMs(incoming);
|
|
79
|
+
const currentMs = capturedAtMs(current);
|
|
80
|
+
if (incomingMs !== null && (currentMs === null || incomingMs > currentMs))
|
|
81
|
+
rows[identity] = incoming;
|
|
223
82
|
}
|
|
224
|
-
const outcome = push(device, payload);
|
|
225
|
-
if (outcome.ok)
|
|
226
|
-
result.pushed.push(item.device);
|
|
227
|
-
else
|
|
228
|
-
result.errors.push({ device: item.device, message: outcome.message ?? 'push failed' });
|
|
229
83
|
}
|
|
84
|
+
result.sources.sort();
|
|
85
|
+
result.merged = ingestPeerClaudeUsageRows(rows, options.cachePath);
|
|
230
86
|
return result;
|
|
231
87
|
}
|
|
@@ -180,8 +180,13 @@ export interface UsageSnapshot {
|
|
|
180
180
|
* MUST NEVER read this field — `isUsageVerified`/`hasStaleUsage`/
|
|
181
181
|
* `hasUsageAvailable`/`deriveUsageStatusFromSnapshot` consult only `windows`,
|
|
182
182
|
* so a stale number rendered here can never make a stale account read as
|
|
183
|
-
* verified or eligible (the RUSH-2858 property).
|
|
184
|
-
*
|
|
183
|
+
* verified or eligible (the RUSH-2858 property). Not a serialized key of its
|
|
184
|
+
* own, and dropped from `--json` (which projects `windows` explicitly) — but
|
|
185
|
+
* the READINGS it holds do round-trip through the on-disk cache:
|
|
186
|
+
* `serializeClaudeUsageSnapshot` persists the union of `windows` and
|
|
187
|
+
* `staleWindows`, and `deserializeClaudeUsageSnapshot` re-runs the freshness
|
|
188
|
+
* gate on read to re-partition them (so a collector like Grok that pre-splits
|
|
189
|
+
* an ended-period reading onto `staleWindows` still survives the round-trip).
|
|
185
190
|
*/
|
|
186
191
|
staleWindows?: UsageWindow[];
|
|
187
192
|
plan?: string | null;
|
|
@@ -229,6 +234,12 @@ interface UsageOptions {
|
|
|
229
234
|
* absent, the backoff stays provider-wide.
|
|
230
235
|
*/
|
|
231
236
|
usageScope?: string | null;
|
|
237
|
+
/**
|
|
238
|
+
* Caller-supplied abort signal (the daemon tick's deadline). Combined with each
|
|
239
|
+
* provider fetch's own timeout so a hung refresh is bounded by BOTH the
|
|
240
|
+
* per-fetch timeout and the supervisor deadline (PHNX-3608).
|
|
241
|
+
*/
|
|
242
|
+
signal?: AbortSignal;
|
|
232
243
|
/**
|
|
233
244
|
* When true, never open the ACL-bound OS keychain item (macOS Touch ID).
|
|
234
245
|
* Daemon usage refresh sets this so a background tick cannot pop biometrics.
|
|
@@ -362,6 +373,8 @@ export declare const USAGE_FETCH_CONCURRENCY = 3;
|
|
|
362
373
|
export interface UsageLookupOptions {
|
|
363
374
|
forceRefresh?: boolean;
|
|
364
375
|
fileOnly?: boolean;
|
|
376
|
+
/** Daemon tick deadline signal, combined with each provider fetch's own timeout (PHNX-3608). */
|
|
377
|
+
signal?: AbortSignal;
|
|
365
378
|
/**
|
|
366
379
|
* Permit a foreground personal-device usage read to fall through to the
|
|
367
380
|
* interactive login when no setup-token exists (USAGE-READ-2). Set ONLY by
|
|
@@ -626,11 +639,11 @@ export interface ProviderProbe {
|
|
|
626
639
|
reason?: 'usage_scope';
|
|
627
640
|
}
|
|
628
641
|
/** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
|
|
629
|
-
export declare function probeClaudeStatus(home?: string, cliVersion?: string | null, usageScope?: string | null): Promise<ProviderProbe>;
|
|
642
|
+
export declare function probeClaudeStatus(home?: string, cliVersion?: string | null, usageScope?: string | null, signal?: AbortSignal): Promise<ProviderProbe>;
|
|
630
643
|
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
631
|
-
export declare function probeKimiStatus(home?: string, usageScope?: string | null): Promise<ProviderProbe>;
|
|
644
|
+
export declare function probeKimiStatus(home?: string, usageScope?: string | null, signal?: AbortSignal): Promise<ProviderProbe>;
|
|
632
645
|
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
633
|
-
export declare function probeDroidStatus(home?: string, usageScope?: string | null): Promise<ProviderProbe>;
|
|
646
|
+
export declare function probeDroidStatus(home?: string, usageScope?: string | null, signal?: AbortSignal): Promise<ProviderProbe>;
|
|
634
647
|
/**
|
|
635
648
|
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
636
649
|
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|