@phnx-labs/agents-cli 1.21.0 → 1.21.2
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 +198 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +63 -40
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/perf.d.ts +13 -0
- package/dist/commands/perf.js +80 -23
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.js +153 -21
- package/dist/commands/routines.js +46 -1
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +14 -1
- package/dist/lib/activity.d.ts +6 -0
- package/dist/lib/activity.js +3 -0
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +18 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/feed-broadcast.d.ts +93 -12
- package/dist/lib/feed-broadcast.js +235 -30
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.js +1 -1
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +14 -4
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +133 -18
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/perf/db.d.ts +6 -1
- package/dist/lib/perf/db.js +35 -5
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +39 -0
- package/dist/lib/runner.js +34 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +14 -3
- package/dist/lib/session/discover.d.ts +3 -0
- package/dist/lib/session/discover.js +8 -0
- package/dist/lib/session/types.d.ts +1 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +14 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
|
@@ -1015,6 +1015,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1015
1015
|
topic: scan.topic,
|
|
1016
1016
|
label,
|
|
1017
1017
|
messageCount: scan.messageCount,
|
|
1018
|
+
toolCallCount: scan.toolCallCount,
|
|
1018
1019
|
tokenCount: scan.tokenCount,
|
|
1019
1020
|
outputTokens: scan.outputTokens,
|
|
1020
1021
|
costUsd: scan.costUsd,
|
|
@@ -1044,6 +1045,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1044
1045
|
model: scan.model,
|
|
1045
1046
|
label,
|
|
1046
1047
|
messageCount: scan.messageCount,
|
|
1048
|
+
toolCallCount: scan.toolCallCount,
|
|
1047
1049
|
tokenCount: scan.tokenCount,
|
|
1048
1050
|
outputTokens: scan.outputTokens,
|
|
1049
1051
|
costUsd: scan.costUsd,
|
|
@@ -2425,6 +2427,7 @@ export function initClaudeParseState() {
|
|
|
2425
2427
|
aiTitle: undefined,
|
|
2426
2428
|
entrypoint: undefined,
|
|
2427
2429
|
messageCount: 0,
|
|
2430
|
+
toolCallCount: 0,
|
|
2428
2431
|
tokenCount: 0,
|
|
2429
2432
|
outputTokens: 0,
|
|
2430
2433
|
sawTokenCount: false,
|
|
@@ -2482,6 +2485,7 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2482
2485
|
for (const b of parsed.message.content) {
|
|
2483
2486
|
if (b?.type !== 'tool_use')
|
|
2484
2487
|
continue;
|
|
2488
|
+
state.toolCallCount++;
|
|
2485
2489
|
if (!state.spawnedTeam && typeof b?.input?.command === 'string') {
|
|
2486
2490
|
const team = detectSpawnedTeam(b.input.command);
|
|
2487
2491
|
if (team)
|
|
@@ -2642,6 +2646,7 @@ export function finalizeClaudeScan(state) {
|
|
|
2642
2646
|
topic: resolvedTopic,
|
|
2643
2647
|
entrypoint: state.entrypoint,
|
|
2644
2648
|
messageCount: state.messageCount,
|
|
2649
|
+
toolCallCount: state.toolCallCount,
|
|
2645
2650
|
tokenCount: state.sawTokenCount ? state.tokenCount : undefined,
|
|
2646
2651
|
outputTokens: state.sawTokenCount ? state.outputTokens : undefined,
|
|
2647
2652
|
costUsd: state.sawCost ? state.costUsd : undefined,
|
|
@@ -2713,6 +2718,7 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2713
2718
|
plan: state.plan,
|
|
2714
2719
|
lastTsMs: state.lastTsMs,
|
|
2715
2720
|
messageCount: state.messageCount,
|
|
2721
|
+
toolCallCount: state.toolCallCount,
|
|
2716
2722
|
tokenCount: state.tokenCount,
|
|
2717
2723
|
outputTokens: state.outputTokens,
|
|
2718
2724
|
sawTokenCount: state.sawTokenCount,
|
|
@@ -2771,6 +2777,7 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2771
2777
|
aiTitle: prior.aiTitle,
|
|
2772
2778
|
entrypoint: prior.entrypoint,
|
|
2773
2779
|
messageCount: prior.messageCount,
|
|
2780
|
+
toolCallCount: prior.toolCallCount ?? 0,
|
|
2774
2781
|
tokenCount: prior.tokenCount,
|
|
2775
2782
|
outputTokens: prior.outputTokens,
|
|
2776
2783
|
sawTokenCount: prior.sawTokenCount,
|
|
@@ -3734,6 +3741,7 @@ export function readCursorMeta(filePath, currentVersion) {
|
|
|
3734
3741
|
topic: firstUserText ? extractSessionTopic(firstUserText) : undefined,
|
|
3735
3742
|
label: title,
|
|
3736
3743
|
messageCount: events.filter((event) => event.type === 'message').length,
|
|
3744
|
+
todos: extractTodoProgressFromEvents(events),
|
|
3737
3745
|
};
|
|
3738
3746
|
return { meta, content: userTexts.join('\n') };
|
|
3739
3747
|
}
|
|
@@ -132,6 +132,7 @@ export interface SessionMeta {
|
|
|
132
132
|
durationMs?: number;
|
|
133
133
|
/** Underlying LLM model observed in the transcript, when the agent records one. */
|
|
134
134
|
model?: string;
|
|
135
|
+
toolCallCount?: number;
|
|
135
136
|
version?: string;
|
|
136
137
|
account?: string;
|
|
137
138
|
topic?: string;
|
|
@@ -71,6 +71,7 @@ export declare const loadFactory: ModuleLoader;
|
|
|
71
71
|
export declare const loadUsage: ModuleLoader;
|
|
72
72
|
export declare const loadCost: ModuleLoader;
|
|
73
73
|
export declare const loadPerf: ModuleLoader;
|
|
74
|
+
export declare const loadTrends: ModuleLoader;
|
|
74
75
|
export declare const loadOutput: ModuleLoader;
|
|
75
76
|
export declare const loadBudget: ModuleLoader;
|
|
76
77
|
export declare const loadAlias: ModuleLoader;
|
|
@@ -49,6 +49,7 @@ export const loadFactory = async () => (await import('../../commands/factory.js'
|
|
|
49
49
|
export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
|
|
50
50
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
51
51
|
export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
|
|
52
|
+
export const loadTrends = async () => (await import('../../commands/trends.js')).registerTrendsCommand;
|
|
52
53
|
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
53
54
|
export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
|
|
54
55
|
export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
|
|
@@ -163,6 +164,7 @@ export const COMMAND_LOADERS = {
|
|
|
163
164
|
usage: [loadUsage],
|
|
164
165
|
cost: [loadCost],
|
|
165
166
|
perf: [loadPerf],
|
|
167
|
+
trends: [loadTrends],
|
|
166
168
|
output: [loadOutput],
|
|
167
169
|
budget: [loadBudget],
|
|
168
170
|
alias: [loadAlias],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -117,6 +117,14 @@ export declare function getUserSecretsDir(): string;
|
|
|
117
117
|
* metadata off the real session flow — not a second write path.
|
|
118
118
|
*/
|
|
119
119
|
export declare function getSecretsDbPath(): string;
|
|
120
|
+
/**
|
|
121
|
+
* Path to the durable resource-usage warehouse (~/.agents/.history/analytics/usage.db).
|
|
122
|
+
* Value-free frequency/lifecycle events (secrets, agents, browser, …). Read at CALL
|
|
123
|
+
* time so AGENTS_USAGE_DB can redirect tests. Sync shards may also appear as
|
|
124
|
+
* usage.<machine-id>.db beside this default file.
|
|
125
|
+
*/
|
|
126
|
+
export declare function getAnalyticsDir(): string;
|
|
127
|
+
export declare function getUsageDbPath(): string;
|
|
120
128
|
export declare function getUserPromptcutsPath(): string;
|
|
121
129
|
/** Canonical home anchor (HOME env override or os.homedir()). */
|
|
122
130
|
export declare function getHomeDir(): string;
|
|
@@ -185,9 +193,20 @@ export declare function getActivityDir(): string;
|
|
|
185
193
|
export declare function getVersionsDir(): string;
|
|
186
194
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
187
195
|
export declare function getShimsDir(): string;
|
|
188
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/).
|
|
198
|
+
* Read at CALL time — since every hook now resolves through a shim (RUSH-2xxx,
|
|
199
|
+
* pass-through timing for matcher-only hooks), a test that registers hooks
|
|
200
|
+
* in-process (no subprocess HOME override) would otherwise write real shim
|
|
201
|
+
* files into the user's actual ~/.agents/.cache. AGENTS_HOOK_SHIMS_DIR mirrors
|
|
202
|
+
* the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR test-isolation escape hatches;
|
|
203
|
+
* never set in production code.
|
|
204
|
+
*/
|
|
189
205
|
export declare function getHookShimsDir(): string;
|
|
190
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). Read at
|
|
208
|
+
* CALL time for the same reason as {@link getHookShimsDir} — see its doc.
|
|
209
|
+
*/
|
|
191
210
|
export declare function getHookCacheDir(): string;
|
|
192
211
|
/** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
|
|
193
212
|
export declare function getBinDir(): string;
|
|
@@ -225,11 +244,20 @@ export declare function getDevicesPendingDir(): string;
|
|
|
225
244
|
export declare function getCloudDir(): string;
|
|
226
245
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
|
227
246
|
export declare function getTerminalsDir(): string;
|
|
228
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Path to runtime logs (~/.agents/.cache/logs/). Read at CALL time so
|
|
249
|
+
* AGENTS_LOGS_DIR can redirect it in tests — same test-isolation escape hatch
|
|
250
|
+
* as {@link getHookShimsDir}; never set in production code.
|
|
251
|
+
*/
|
|
229
252
|
export declare function getLogsDir(): string;
|
|
230
253
|
/**
|
|
231
254
|
* Path to disposable performance samples (~/.agents/.cache/perf/).
|
|
232
255
|
* Holds `perf.db` + a hook-shim spool. Loss is acceptable — wipe freely.
|
|
256
|
+
* Read at CALL time: AGENTS_PERF_DIR (the same override perf/db.ts and
|
|
257
|
+
* perf/spool.ts already honor for their own internal resolution) redirects
|
|
258
|
+
* this canonical getter too, so a caller that goes through it directly
|
|
259
|
+
* (hooks/cache.ts's shim generator, the OpenCode timeout sample writer in
|
|
260
|
+
* hooks.ts) doesn't leak samples into the user's real perf warehouse either.
|
|
233
261
|
*/
|
|
234
262
|
export declare function getPerfDir(): string;
|
|
235
263
|
/** Path to the perf SQLite warehouse (~/.agents/.cache/perf/perf.db). */
|
package/dist/lib/state.js
CHANGED
|
@@ -96,6 +96,7 @@ const PROJECTS_DIR = path.join(USER_AGENTS_DIR, 'projects');
|
|
|
96
96
|
// History bucket (durable).
|
|
97
97
|
const SESSIONS_DIR = path.join(HISTORY_DIR, 'sessions');
|
|
98
98
|
const SESSIONS_DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
|
|
99
|
+
const ANALYTICS_DIR = path.join(HISTORY_DIR, 'analytics');
|
|
99
100
|
const VERSIONS_DIR = path.join(HISTORY_DIR, 'versions');
|
|
100
101
|
const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
|
|
101
102
|
// Durable per-monitor state-diff store + fire history (last-seen value/hash,
|
|
@@ -377,6 +378,18 @@ export function getUserSecretsDir() { return USER_SECRETS_DIR; }
|
|
|
377
378
|
export function getSecretsDbPath() {
|
|
378
379
|
return process.env.AGENTS_SECRETS_DB ?? path.join(USER_SECRETS_DIR, 'secrets.db');
|
|
379
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Path to the durable resource-usage warehouse (~/.agents/.history/analytics/usage.db).
|
|
383
|
+
* Value-free frequency/lifecycle events (secrets, agents, browser, …). Read at CALL
|
|
384
|
+
* time so AGENTS_USAGE_DB can redirect tests. Sync shards may also appear as
|
|
385
|
+
* usage.<machine-id>.db beside this default file.
|
|
386
|
+
*/
|
|
387
|
+
export function getAnalyticsDir() {
|
|
388
|
+
return process.env.AGENTS_ANALYTICS_DIR ?? ANALYTICS_DIR;
|
|
389
|
+
}
|
|
390
|
+
export function getUsageDbPath() {
|
|
391
|
+
return process.env.AGENTS_USAGE_DB ?? path.join(getAnalyticsDir(), 'usage.db');
|
|
392
|
+
}
|
|
380
393
|
export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
|
|
381
394
|
// ─── User operational path getters ────────────────────────────────────────────
|
|
382
395
|
//
|
|
@@ -459,10 +472,25 @@ export function getActivityDir() { return ACTIVITY_DIR; }
|
|
|
459
472
|
export function getVersionsDir() { return VERSIONS_DIR; }
|
|
460
473
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
461
474
|
export function getShimsDir() { return SHIMS_DIR; }
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/).
|
|
477
|
+
* Read at CALL time — since every hook now resolves through a shim (RUSH-2xxx,
|
|
478
|
+
* pass-through timing for matcher-only hooks), a test that registers hooks
|
|
479
|
+
* in-process (no subprocess HOME override) would otherwise write real shim
|
|
480
|
+
* files into the user's actual ~/.agents/.cache. AGENTS_HOOK_SHIMS_DIR mirrors
|
|
481
|
+
* the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR test-isolation escape hatches;
|
|
482
|
+
* never set in production code.
|
|
483
|
+
*/
|
|
484
|
+
export function getHookShimsDir() {
|
|
485
|
+
return process.env.AGENTS_HOOK_SHIMS_DIR ?? HOOK_SHIMS_DIR;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). Read at
|
|
489
|
+
* CALL time for the same reason as {@link getHookShimsDir} — see its doc.
|
|
490
|
+
*/
|
|
491
|
+
export function getHookCacheDir() {
|
|
492
|
+
return process.env.AGENTS_HOOK_CACHE_DIR ?? HOOK_CACHE_DIR;
|
|
493
|
+
}
|
|
466
494
|
/** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
|
|
467
495
|
export function getBinDir() { return BIN_DIR; }
|
|
468
496
|
/** Path to config backups (~/.agents/.history/backups/). */
|
|
@@ -487,7 +515,9 @@ export function getTrashDir() { return TRASH_DIR; }
|
|
|
487
515
|
/** Path to local session indexer storage (~/.agents/.history/sessions/). */
|
|
488
516
|
export function getSessionsDir() { return SESSIONS_DIR; }
|
|
489
517
|
/** Path to the session index database (~/.agents/.history/sessions/sessions.db). */
|
|
490
|
-
export function getSessionsDbPath() {
|
|
518
|
+
export function getSessionsDbPath() {
|
|
519
|
+
return process.env.AGENTS_SESSIONS_DB ?? SESSIONS_DB_PATH;
|
|
520
|
+
}
|
|
491
521
|
/** Path to teams config + registry (~/.agents/teams/). */
|
|
492
522
|
export function getTeamsDir() { return TEAMS_DIR; }
|
|
493
523
|
/** Path to teams execution history (~/.agents/.history/teams/agents/). */
|
|
@@ -517,17 +547,30 @@ export function getDevicesPendingDir() { return path.join(RUNTIME_STATE_DIR, 'de
|
|
|
517
547
|
export function getCloudDir() { return CLOUD_DIR; }
|
|
518
548
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
|
519
549
|
export function getTerminalsDir() { return TERMINALS_DIR; }
|
|
520
|
-
/**
|
|
521
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Path to runtime logs (~/.agents/.cache/logs/). Read at CALL time so
|
|
552
|
+
* AGENTS_LOGS_DIR can redirect it in tests — same test-isolation escape hatch
|
|
553
|
+
* as {@link getHookShimsDir}; never set in production code.
|
|
554
|
+
*/
|
|
555
|
+
export function getLogsDir() {
|
|
556
|
+
return process.env.AGENTS_LOGS_DIR ?? LOGS_DIR;
|
|
557
|
+
}
|
|
522
558
|
/**
|
|
523
559
|
* Path to disposable performance samples (~/.agents/.cache/perf/).
|
|
524
560
|
* Holds `perf.db` + a hook-shim spool. Loss is acceptable — wipe freely.
|
|
561
|
+
* Read at CALL time: AGENTS_PERF_DIR (the same override perf/db.ts and
|
|
562
|
+
* perf/spool.ts already honor for their own internal resolution) redirects
|
|
563
|
+
* this canonical getter too, so a caller that goes through it directly
|
|
564
|
+
* (hooks/cache.ts's shim generator, the OpenCode timeout sample writer in
|
|
565
|
+
* hooks.ts) doesn't leak samples into the user's real perf warehouse either.
|
|
525
566
|
*/
|
|
526
|
-
export function getPerfDir() {
|
|
567
|
+
export function getPerfDir() {
|
|
568
|
+
return process.env.AGENTS_PERF_DIR ?? PERF_DIR;
|
|
569
|
+
}
|
|
527
570
|
/** Path to the perf SQLite warehouse (~/.agents/.cache/perf/perf.db). */
|
|
528
|
-
export function getPerfDbPath() { return path.join(
|
|
571
|
+
export function getPerfDbPath() { return path.join(getPerfDir(), 'perf.db'); }
|
|
529
572
|
/** Path to the hook-shim NDJSON spool drained into perf.db on open. */
|
|
530
|
-
export function getPerfSpoolPath() { return path.join(
|
|
573
|
+
export function getPerfSpoolPath() { return path.join(getPerfDir(), 'spool.jsonl'); }
|
|
531
574
|
/** Path to per-process runtime state (~/.agents/.cache/state/). */
|
|
532
575
|
export function getRuntimeStateDir() { return RUNTIME_STATE_DIR; }
|
|
533
576
|
/** Path to companion-extension scratch (~/.agents/.cache/companion/). */
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -284,6 +284,12 @@ export type HookCache = string | HookCacheConfig;
|
|
|
284
284
|
export interface ManifestHook {
|
|
285
285
|
script: string;
|
|
286
286
|
events: string[];
|
|
287
|
+
/**
|
|
288
|
+
* Seconds before the hook is killed (default 600). In agents.yaml this may be
|
|
289
|
+
* written as a bare number (seconds) or a duration string (`5s`, `2m`,
|
|
290
|
+
* `1h30m`); `parseHookManifest` normalizes it to a seconds number here, so
|
|
291
|
+
* consumers always see a number.
|
|
292
|
+
*/
|
|
287
293
|
timeout?: number;
|
|
288
294
|
matcher?: string;
|
|
289
295
|
/** @deprecated Use the agent capability table; field is ignored. */
|
|
@@ -765,10 +771,14 @@ export interface Meta {
|
|
|
765
771
|
/** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
|
|
766
772
|
budget?: BudgetConfig;
|
|
767
773
|
/**
|
|
768
|
-
* `agents feed post` fan-out. `broadcast` maps a sink name to
|
|
769
|
-
* run for each post
|
|
770
|
-
*
|
|
771
|
-
*
|
|
774
|
+
* `agents feed post` fan-out. `broadcast` maps a sink name to either an argv
|
|
775
|
+
* template (`command:`, run for each post) or an in-process channel delivery
|
|
776
|
+
* (`channel:`, the same registry `agents send`/`agents notify` use), so
|
|
777
|
+
* mirroring to a tracker, a messaging CLI, or a channel provider is the
|
|
778
|
+
* operator's config rather than an integration compiled into this CLI. When
|
|
779
|
+
* this is unset/empty, an important-level post falls back to `notify.owner`
|
|
780
|
+
* implicitly (RUSH-2123) — see lib/feed-broadcast.ts and
|
|
781
|
+
* docs/06-observability.md.
|
|
772
782
|
*/
|
|
773
783
|
feed?: {
|
|
774
784
|
broadcast?: FeedBroadcastConfig;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type UsageHeadroom, type UsageSnapshot, type UsageInfo } from './usage.js';
|
|
2
|
+
import type { AgentId } from './types.js';
|
|
3
|
+
/** Floor on the adaptive interval: an account seconds from its cap still isn't
|
|
4
|
+
* polled faster than this. */
|
|
5
|
+
export declare const REFRESH_MIN_MS: number;
|
|
6
|
+
/** Ceiling on the adaptive interval: an idle account is still re-checked at
|
|
7
|
+
* least this often so a cache never silently rots. */
|
|
8
|
+
export declare const REFRESH_MAX_MS: number;
|
|
9
|
+
/** Schedule the next refresh at `minutesToLimit / K` — poll well before the cap,
|
|
10
|
+
* not exactly at it. */
|
|
11
|
+
export declare const REFRESH_BURN_DIVISOR = 4;
|
|
12
|
+
/** At most this many live fetches per account per rolling hour. */
|
|
13
|
+
export declare const HOURLY_CALL_CAP = 6;
|
|
14
|
+
/**
|
|
15
|
+
* One account's refresh state + published headroom. `sessionUsedPercent` /
|
|
16
|
+
* `capturedAt` are the prior sample the NEXT tick projects the burn rate from;
|
|
17
|
+
* `minutesToLimit` / `status` are what the routing hot path reads.
|
|
18
|
+
*/
|
|
19
|
+
export interface HeadroomEntry {
|
|
20
|
+
status: UsageHeadroom['status'];
|
|
21
|
+
minutesToLimit: number | null;
|
|
22
|
+
/** The session window's usedPercent in the last snapshot (the prev sample). */
|
|
23
|
+
sessionUsedPercent: number | null;
|
|
24
|
+
/** Epoch ms the last snapshot was captured. */
|
|
25
|
+
capturedAt: number | null;
|
|
26
|
+
/** Epoch ms this account is next due for a live refresh. */
|
|
27
|
+
nextRefreshAt: number;
|
|
28
|
+
/** Epoch ms of recent live fetches, for the rolling-hour cap. */
|
|
29
|
+
callTimestamps: number[];
|
|
30
|
+
/** Epoch ms this entry was written. */
|
|
31
|
+
computedAt: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function setHeadroomCachePathForTest(cachePath: string | null): string | null;
|
|
34
|
+
/** Read the whole headroom cache (best-effort; missing/corrupt → empty map). */
|
|
35
|
+
export declare function readHeadroomCache(): Record<string, HeadroomEntry>;
|
|
36
|
+
/** Read one account's headroom entry, or null. */
|
|
37
|
+
export declare function readHeadroomEntry(usageKey: string): HeadroomEntry | null;
|
|
38
|
+
/** Merge entries into the cache (best-effort; preserves other accounts' rows). */
|
|
39
|
+
export declare function writeHeadroomEntries(entries: Record<string, HeadroomEntry>): void;
|
|
40
|
+
/**
|
|
41
|
+
* The adaptive interval until the next refresh, clamped to [MIN, MAX]. A
|
|
42
|
+
* shorter `minutesToLimit` (closer to the cap) polls sooner; `null` (unknown /
|
|
43
|
+
* idle / not burning) waits the full ceiling.
|
|
44
|
+
*/
|
|
45
|
+
export declare function computeNextRefreshDelayMs(minutesToLimit: number | null, opts?: {
|
|
46
|
+
minMs?: number;
|
|
47
|
+
maxMs?: number;
|
|
48
|
+
divisor?: number;
|
|
49
|
+
}): number;
|
|
50
|
+
/** Recent call timestamps trimmed to the trailing hour. */
|
|
51
|
+
export declare function pruneCallTimestamps(timestamps: number[], now: number, windowMs?: number): number[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether an account may be live-refreshed right now: it is due (past its
|
|
54
|
+
* scheduled `nextRefreshAt`) AND under the rolling-hour call cap. Pure so the
|
|
55
|
+
* cadence + cap arithmetic is unit-tested without a daemon or a network call.
|
|
56
|
+
*/
|
|
57
|
+
export declare function shouldRefreshAccount(entry: HeadroomEntry | null | undefined, now: number, opts?: {
|
|
58
|
+
hourlyCap?: number;
|
|
59
|
+
windowMs?: number;
|
|
60
|
+
}): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Build the next headroom entry after a live refresh: project headroom from the
|
|
63
|
+
* new snapshot against the prior sample, schedule the next refresh from the
|
|
64
|
+
* projection, and record this call for the hourly cap.
|
|
65
|
+
*/
|
|
66
|
+
export declare function nextHeadroomEntry(prev: HeadroomEntry | null | undefined, snapshot: UsageSnapshot | null, now: number): HeadroomEntry;
|
|
67
|
+
/** An account whose credentials live on THIS host — the only ones we refresh. */
|
|
68
|
+
export interface LocalUsageAccount {
|
|
69
|
+
usageKey: string;
|
|
70
|
+
agentId: AgentId;
|
|
71
|
+
/** Live-fetch this account's usage; the daemon passes the real network fetch. */
|
|
72
|
+
fetch: () => Promise<UsageInfo>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Enumerate the network-usage accounts whose credentials live on THIS host — one
|
|
76
|
+
* per unique usage key, deduped to the most-recently-active version (the same
|
|
77
|
+
* canonicalization `getUsageInfoByIdentity` uses). Each carries a closure that
|
|
78
|
+
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
79
|
+
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
80
|
+
* accounts' caches — no cross-host coordination.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildLocalUsageAccounts(): Promise<LocalUsageAccount[]>;
|
|
83
|
+
/** Injectable side effects, so `runUsageRefresh` is drivable without the daemon. */
|
|
84
|
+
export interface UsageRefreshDeps {
|
|
85
|
+
now?: number;
|
|
86
|
+
/** Local-credential accounts to consider (one per unique usage key). */
|
|
87
|
+
listAccounts: () => Promise<LocalUsageAccount[]>;
|
|
88
|
+
/** Persist a fresh snapshot to the usage cache (writeClaudeUsageCache). */
|
|
89
|
+
writeUsageCache: (usageKey: string, snapshot: UsageSnapshot) => void;
|
|
90
|
+
/** Epoch ms a provider is backed off until, or null when free (usageRateLimitedUntil). */
|
|
91
|
+
backoffUntil: (agentId: AgentId) => number | null;
|
|
92
|
+
}
|
|
93
|
+
export interface UsageRefreshResult {
|
|
94
|
+
refreshed: number;
|
|
95
|
+
skippedNotDue: number;
|
|
96
|
+
skippedBackoff: number;
|
|
97
|
+
skippedCap: number;
|
|
98
|
+
failed: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* One refresher tick: for each local account that is due, under its hourly cap,
|
|
102
|
+
* and not provider-backed-off, live-fetch its usage, update the cache, and
|
|
103
|
+
* reschedule from the new burn projection. Never throws — a single account's
|
|
104
|
+
* failed fetch leaves its cache untouched and counts as `failed`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runUsageRefresh(deps: UsageRefreshDeps): Promise<UsageRefreshResult>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-owned adaptive usage refresher.
|
|
3
|
+
*
|
|
4
|
+
* The routing hot path (`agents run` → collectRunCandidates) reads usage
|
|
5
|
+
* CACHE-ONLY (`getUsageInfoForIdentity` `readOnly`, RUSH-2061) and never blocks
|
|
6
|
+
* on a provider fetch. Something still has to keep that cache fresh — this
|
|
7
|
+
* module is that something, running inside the daemon.
|
|
8
|
+
*
|
|
9
|
+
* Design (per account, this host is the SOLE writer — the refresher only ever
|
|
10
|
+
* touches accounts whose credentials live locally, so no cross-host
|
|
11
|
+
* coordination and no shared-token contention):
|
|
12
|
+
*
|
|
13
|
+
* - **Adaptive cadence from burn rate.** Each refresh stores the session
|
|
14
|
+
* window's `usedPercent` + `capturedAt`. The next refresh is scheduled from
|
|
15
|
+
* `deriveUsageHeadroom`'s projected `minutesToLimit`: an account racing
|
|
16
|
+
* toward its cap is polled more often (down to 90s), an idle one rarely (up
|
|
17
|
+
* to 15min). `computeNextRefreshDelayMs` is the pure clamp.
|
|
18
|
+
* - **Hard hourly cap.** Regardless of cadence, at most `HOURLY_CALL_CAP` live
|
|
19
|
+
* fetches per account per rolling hour, so a fast burn can't turn the 90s
|
|
20
|
+
* floor into a hammering loop.
|
|
21
|
+
* - **Respects the existing 429 backoff.** A provider under
|
|
22
|
+
* `usageRateLimitedUntil` is skipped entirely — the whole point of the
|
|
23
|
+
* backoff is to stop poking an endpoint that just said no.
|
|
24
|
+
*
|
|
25
|
+
* It also publishes the projected headroom (`minutesToLimit` + status) to a
|
|
26
|
+
* small cache keyed by usage key, so the routing hot path can deprioritize an
|
|
27
|
+
* account projected to cap without recomputing a burn rate it has no prior
|
|
28
|
+
* sample for. `fleet-cache.ts` is the sync reader.
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'fs';
|
|
31
|
+
import * as path from 'path';
|
|
32
|
+
import { getCacheDir } from './state.js';
|
|
33
|
+
import { deriveUsageHeadroom, getUsageInfo, buildCanonicalUsageContext, agentUsesNetworkUsage, USAGE_SOURCE_AGENT_IDS, } from './usage.js';
|
|
34
|
+
import { getAccountInfo } from './agents.js';
|
|
35
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
36
|
+
/** Floor on the adaptive interval: an account seconds from its cap still isn't
|
|
37
|
+
* polled faster than this. */
|
|
38
|
+
export const REFRESH_MIN_MS = 90 * 1000;
|
|
39
|
+
/** Ceiling on the adaptive interval: an idle account is still re-checked at
|
|
40
|
+
* least this often so a cache never silently rots. */
|
|
41
|
+
export const REFRESH_MAX_MS = 15 * 60 * 1000;
|
|
42
|
+
/** Schedule the next refresh at `minutesToLimit / K` — poll well before the cap,
|
|
43
|
+
* not exactly at it. */
|
|
44
|
+
export const REFRESH_BURN_DIVISOR = 4;
|
|
45
|
+
/** At most this many live fetches per account per rolling hour. */
|
|
46
|
+
export const HOURLY_CALL_CAP = 6;
|
|
47
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
48
|
+
/** Test seam for the headroom cache path (see usage.ts `setClaudeUsageCachePathForTest`). */
|
|
49
|
+
let headroomCachePathOverride = null;
|
|
50
|
+
export function setHeadroomCachePathForTest(cachePath) {
|
|
51
|
+
const prev = headroomCachePathOverride;
|
|
52
|
+
headroomCachePathOverride = cachePath;
|
|
53
|
+
return prev;
|
|
54
|
+
}
|
|
55
|
+
function headroomCachePath() {
|
|
56
|
+
return headroomCachePathOverride ?? path.join(getCacheDir(), '.usage-headroom.json');
|
|
57
|
+
}
|
|
58
|
+
/** Read the whole headroom cache (best-effort; missing/corrupt → empty map). */
|
|
59
|
+
export function readHeadroomCache() {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(fs.readFileSync(headroomCachePath(), 'utf-8'));
|
|
62
|
+
if (parsed && parsed.entries && typeof parsed.entries === 'object')
|
|
63
|
+
return parsed.entries;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// missing or corrupt — treat as empty
|
|
67
|
+
}
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
/** Read one account's headroom entry, or null. */
|
|
71
|
+
export function readHeadroomEntry(usageKey) {
|
|
72
|
+
return readHeadroomCache()[usageKey] ?? null;
|
|
73
|
+
}
|
|
74
|
+
/** Merge entries into the cache (best-effort; preserves other accounts' rows). */
|
|
75
|
+
export function writeHeadroomEntries(entries) {
|
|
76
|
+
try {
|
|
77
|
+
const dir = getCacheDir();
|
|
78
|
+
if (!fs.existsSync(dir))
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
const merged = {
|
|
81
|
+
version: 1,
|
|
82
|
+
entries: { ...readHeadroomCache(), ...entries },
|
|
83
|
+
};
|
|
84
|
+
fs.writeFileSync(headroomCachePath(), JSON.stringify(merged, null, 2));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// best-effort; a failed write just means the router sees no projection
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The adaptive interval until the next refresh, clamped to [MIN, MAX]. A
|
|
92
|
+
* shorter `minutesToLimit` (closer to the cap) polls sooner; `null` (unknown /
|
|
93
|
+
* idle / not burning) waits the full ceiling.
|
|
94
|
+
*/
|
|
95
|
+
export function computeNextRefreshDelayMs(minutesToLimit, opts = {}) {
|
|
96
|
+
const minMs = opts.minMs ?? REFRESH_MIN_MS;
|
|
97
|
+
const maxMs = opts.maxMs ?? REFRESH_MAX_MS;
|
|
98
|
+
const divisor = opts.divisor ?? REFRESH_BURN_DIVISOR;
|
|
99
|
+
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
100
|
+
return maxMs;
|
|
101
|
+
const targetMs = (minutesToLimit / divisor) * 60_000;
|
|
102
|
+
return Math.max(minMs, Math.min(maxMs, targetMs));
|
|
103
|
+
}
|
|
104
|
+
/** Recent call timestamps trimmed to the trailing hour. */
|
|
105
|
+
export function pruneCallTimestamps(timestamps, now, windowMs = HOUR_MS) {
|
|
106
|
+
const floor = now - windowMs;
|
|
107
|
+
return timestamps.filter((ts) => ts > floor);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether an account may be live-refreshed right now: it is due (past its
|
|
111
|
+
* scheduled `nextRefreshAt`) AND under the rolling-hour call cap. Pure so the
|
|
112
|
+
* cadence + cap arithmetic is unit-tested without a daemon or a network call.
|
|
113
|
+
*/
|
|
114
|
+
export function shouldRefreshAccount(entry, now, opts = {}) {
|
|
115
|
+
const cap = opts.hourlyCap ?? HOURLY_CALL_CAP;
|
|
116
|
+
const windowMs = opts.windowMs ?? HOUR_MS;
|
|
117
|
+
const due = !entry || now >= entry.nextRefreshAt;
|
|
118
|
+
if (!due)
|
|
119
|
+
return false;
|
|
120
|
+
const recent = pruneCallTimestamps(entry?.callTimestamps ?? [], now, windowMs);
|
|
121
|
+
return recent.length < cap;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build the next headroom entry after a live refresh: project headroom from the
|
|
125
|
+
* new snapshot against the prior sample, schedule the next refresh from the
|
|
126
|
+
* projection, and record this call for the hourly cap.
|
|
127
|
+
*/
|
|
128
|
+
export function nextHeadroomEntry(prev, snapshot, now) {
|
|
129
|
+
const headroom = deriveUsageHeadroom(snapshot, prev && prev.capturedAt !== null && prev.sessionUsedPercent !== null
|
|
130
|
+
? { capturedAt: prev.capturedAt, usedPercent: prev.sessionUsedPercent }
|
|
131
|
+
: null);
|
|
132
|
+
const session = snapshot?.windows.find((window) => window.key === 'session') ?? null;
|
|
133
|
+
const callTimestamps = pruneCallTimestamps([...(prev?.callTimestamps ?? []), now], now);
|
|
134
|
+
return {
|
|
135
|
+
status: headroom.status,
|
|
136
|
+
minutesToLimit: headroom.minutesToLimit,
|
|
137
|
+
sessionUsedPercent: session?.usedPercent ?? null,
|
|
138
|
+
capturedAt: snapshot?.capturedAt?.getTime() ?? null,
|
|
139
|
+
nextRefreshAt: now + computeNextRefreshDelayMs(headroom.minutesToLimit),
|
|
140
|
+
callTimestamps,
|
|
141
|
+
computedAt: now,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Enumerate the network-usage accounts whose credentials live on THIS host — one
|
|
146
|
+
* per unique usage key, deduped to the most-recently-active version (the same
|
|
147
|
+
* canonicalization `getUsageInfoByIdentity` uses). Each carries a closure that
|
|
148
|
+
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
149
|
+
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
150
|
+
* accounts' caches — no cross-host coordination.
|
|
151
|
+
*/
|
|
152
|
+
export async function buildLocalUsageAccounts() {
|
|
153
|
+
const accounts = [];
|
|
154
|
+
for (const agentId of USAGE_SOURCE_AGENT_IDS) {
|
|
155
|
+
if (!agentUsesNetworkUsage(agentId))
|
|
156
|
+
continue;
|
|
157
|
+
const versions = listInstalledVersions(agentId);
|
|
158
|
+
if (versions.length === 0)
|
|
159
|
+
continue;
|
|
160
|
+
const inputs = await Promise.all(versions.map(async (version) => {
|
|
161
|
+
const home = getVersionHomePath(agentId, version);
|
|
162
|
+
return { agentId, info: await getAccountInfo(agentId, home), home, cliVersion: version };
|
|
163
|
+
}));
|
|
164
|
+
const { canonicalByUsageKey, usageFetchInputs } = buildCanonicalUsageContext(inputs);
|
|
165
|
+
for (const [usageKey, fetchInput] of usageFetchInputs) {
|
|
166
|
+
const canonical = canonicalByUsageKey.get(usageKey);
|
|
167
|
+
if (!canonical?.signedIn)
|
|
168
|
+
continue; // only refresh accounts actually usable here
|
|
169
|
+
accounts.push({
|
|
170
|
+
usageKey,
|
|
171
|
+
agentId,
|
|
172
|
+
fetch: () => getUsageInfo(agentId, {
|
|
173
|
+
home: fetchInput.home,
|
|
174
|
+
cliVersion: fetchInput.cliVersion,
|
|
175
|
+
organizationId: fetchInput.organizationId,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return accounts;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* One refresher tick: for each local account that is due, under its hourly cap,
|
|
184
|
+
* and not provider-backed-off, live-fetch its usage, update the cache, and
|
|
185
|
+
* reschedule from the new burn projection. Never throws — a single account's
|
|
186
|
+
* failed fetch leaves its cache untouched and counts as `failed`.
|
|
187
|
+
*/
|
|
188
|
+
export async function runUsageRefresh(deps) {
|
|
189
|
+
const now = deps.now ?? Date.now();
|
|
190
|
+
const result = {
|
|
191
|
+
refreshed: 0,
|
|
192
|
+
skippedNotDue: 0,
|
|
193
|
+
skippedBackoff: 0,
|
|
194
|
+
skippedCap: 0,
|
|
195
|
+
failed: 0,
|
|
196
|
+
};
|
|
197
|
+
const accounts = await deps.listAccounts();
|
|
198
|
+
const cache = readHeadroomCache();
|
|
199
|
+
const updates = {};
|
|
200
|
+
for (const account of accounts) {
|
|
201
|
+
const entry = cache[account.usageKey] ?? null;
|
|
202
|
+
// A provider under a 429 penalty is off-limits — poking it re-arms the
|
|
203
|
+
// penalty (the whole reason usage-backoff exists).
|
|
204
|
+
if ((deps.backoffUntil(account.agentId) ?? 0) > now) {
|
|
205
|
+
result.skippedBackoff += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!shouldRefreshAccount(entry, now)) {
|
|
209
|
+
if (entry && now < entry.nextRefreshAt)
|
|
210
|
+
result.skippedNotDue += 1;
|
|
211
|
+
else
|
|
212
|
+
result.skippedCap += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const usage = await account.fetch();
|
|
217
|
+
if (usage.snapshot?.source === 'live') {
|
|
218
|
+
deps.writeUsageCache(account.usageKey, usage.snapshot);
|
|
219
|
+
updates[account.usageKey] = nextHeadroomEntry(entry, usage.snapshot, now);
|
|
220
|
+
result.refreshed += 1;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// No live snapshot (expired token / fetch miss): don't rewrite the usage
|
|
224
|
+
// cache, but still record the call + reschedule so a broken account
|
|
225
|
+
// isn't retried every tick.
|
|
226
|
+
updates[account.usageKey] = nextHeadroomEntry(entry, null, now);
|
|
227
|
+
result.failed += 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
updates[account.usageKey] = nextHeadroomEntry(entry, null, now);
|
|
232
|
+
result.failed += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(updates).length > 0)
|
|
236
|
+
writeHeadroomEntries(updates);
|
|
237
|
+
return result;
|
|
238
|
+
}
|