@phnx-labs/agents-cli 1.22.109 → 1.22.111
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 +48 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/browser-sessions-picker.js +2 -1
- package/dist/commands/computer-sessions-picker.js +2 -1
- package/dist/commands/cost.js +2 -1
- package/dist/commands/fork.js +6 -4
- package/dist/commands/logs.js +2 -1
- package/dist/commands/sessions-backfill.d.ts +30 -0
- package/dist/commands/sessions-backfill.js +72 -0
- package/dist/commands/sessions-inject.d.ts +3 -3
- package/dist/commands/sessions-inject.js +5 -11
- package/dist/commands/sessions-picker.js +10 -7
- package/dist/commands/sessions-resume.d.ts +3 -1
- package/dist/commands/sessions-resume.js +32 -3
- package/dist/commands/sessions.js +44 -24
- package/dist/commands/utils.d.ts +9 -0
- package/dist/commands/utils.js +18 -0
- package/dist/commands/watchdog.js +4 -1
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +17 -1
- package/dist/lib/accounting/usage-sync.d.ts +22 -0
- package/dist/lib/accounting/usage-sync.js +70 -6
- package/dist/lib/accounting/usage.d.ts +32 -0
- package/dist/lib/accounting/usage.js +37 -2
- package/dist/lib/auth-health.js +19 -0
- package/dist/lib/claude-statusline.js +5 -0
- package/dist/lib/computer/sessions-list.js +2 -1
- package/dist/lib/daemon/auth-sync-service.d.ts +2 -0
- package/dist/lib/daemon/auth-sync-service.js +2 -2
- package/dist/lib/daemon/daemon.js +7 -0
- package/dist/lib/daemon/session-title-service.d.ts +41 -0
- package/dist/lib/daemon/session-title-service.js +76 -0
- package/dist/lib/daemon/usage-sync-service.d.ts +9 -1
- package/dist/lib/daemon/usage-sync-service.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.js +6 -0
- package/dist/lib/fleet-shared-state.d.ts +2 -0
- package/dist/lib/hooks/install.js +14 -7
- package/dist/lib/mailbox-target.js +2 -1
- package/dist/lib/remote-agents-json.js +1 -1
- package/dist/lib/session/active.d.ts +93 -16
- package/dist/lib/session/active.js +80 -14
- package/dist/lib/session/db.d.ts +44 -1
- package/dist/lib/session/db.js +122 -17
- package/dist/lib/session/fork.d.ts +10 -2
- package/dist/lib/session/fork.js +11 -2
- package/dist/lib/session/live-metadata.js +6 -0
- package/dist/lib/session/mirror.d.ts +3 -2
- package/dist/lib/session/mirror.js +5 -2
- package/dist/lib/session/remote/remote-list.js +1 -1
- package/dist/lib/session/remote/watch.js +18 -8
- package/dist/lib/session/title.d.ts +256 -0
- package/dist/lib/session/title.js +312 -0
- package/dist/lib/session/tool-index.d.ts +5 -0
- package/dist/lib/session/tool-index.js +1 -0
- package/dist/lib/session/types.d.ts +11 -0
- package/dist/lib/startup/root-command.d.ts +2 -0
- package/dist/lib/startup/root-command.js +22 -0
- package/dist/lib/traces/sync.js +1 -0
- package/dist/lib/usage-refresh.d.ts +58 -7
- package/dist/lib/usage-refresh.js +145 -23
- package/dist/lib/watchdog/runner.d.ts +3 -0
- package/dist/lib/watchdog/runner.js +1 -0
- package/dist/session-tracker/dist/install-hook.js +4 -4
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ import { inferSessionState } from '../lib/session/state.js';
|
|
|
37
37
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
38
38
|
import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
|
|
39
39
|
import { liveSessionMetas, fleetExecutionMachineById, reconcileLiveMetaMachine } from '../lib/session/live-metadata.js';
|
|
40
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
40
41
|
import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
41
42
|
import { parseSession } from '../lib/session/parse.js';
|
|
42
43
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
@@ -351,6 +352,7 @@ export function buildSessionDescription(s) {
|
|
|
351
352
|
return cleanPreview(parts.filter(Boolean).join(' · '));
|
|
352
353
|
}
|
|
353
354
|
// Terminal, headless, or sub-agent: todos + live preview, then label, then topic.
|
|
355
|
+
// ladder-exempt: the compact --active preview BASE (a live snippet), not the row's headline title.
|
|
354
356
|
const base = s.preview || s.label || s.topic || '';
|
|
355
357
|
return cleanPreview([todo, base].filter(Boolean).join(' · '));
|
|
356
358
|
}
|
|
@@ -759,22 +761,39 @@ export function renderActiveRowLines(s, indent, termW) {
|
|
|
759
761
|
// Line 2: label/topic + checklist (the identity, no longer buried) then the
|
|
760
762
|
// jump locator. Skipped entirely when there is nothing to say. desc may carry a
|
|
761
763
|
// clickable project link, so it also goes through fitCell.
|
|
764
|
+
const contIndent = indent + ' '.repeat(ROW_ID_W);
|
|
762
765
|
const desc = formatActiveRowDescription(s);
|
|
763
766
|
const loc = locatorBadge(s);
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
767
|
+
if (desc || loc) {
|
|
768
|
+
const room2 = Math.max(0, termW - stringWidth(contIndent) - 2);
|
|
769
|
+
const locCell = fitCell(loc, room2);
|
|
770
|
+
const locW = stringWidth(locCell);
|
|
771
|
+
const descRoom = Math.max(0, room2 - (locW ? locW + 2 : 0));
|
|
772
|
+
const descCell = chalk.white(fitCell(desc || '-', descRoom));
|
|
773
|
+
let line2 = contIndent + chalk.dim('└ ') + descCell;
|
|
774
|
+
if (locCell)
|
|
775
|
+
line2 += ' ' + locCell;
|
|
776
|
+
if (stringWidth(line2) > termW)
|
|
777
|
+
line2 = truncateToWidth(line2, termW);
|
|
778
|
+
lines.push(line2);
|
|
779
|
+
}
|
|
780
|
+
// Secondary line (PHNX-3797 owner feedback): surface the most important recent
|
|
781
|
+
// agent message when it is a pending QUESTION or a NEEDS-YOU block — the urgency
|
|
782
|
+
// a rolling activity preview buries. Plain `activity` is already the preview on
|
|
783
|
+
// line 2, so it earns no extra line here; the full ranked message still rides
|
|
784
|
+
// the JSON/mirror feed for AGI EXT via `s.importantMessage`.
|
|
785
|
+
const important = s.importantMessage;
|
|
786
|
+
if (important && (important.kind === 'question' || important.kind === 'needs_you')) {
|
|
787
|
+
const glyph = important.kind === 'question' ? '? ' : '! ';
|
|
788
|
+
const room3 = Math.max(0, termW - stringWidth(contIndent) - 2 - glyph.length);
|
|
789
|
+
const msgCell = fitCell(cleanPreview(important.text), room3);
|
|
790
|
+
if (msgCell) {
|
|
791
|
+
let line3 = contIndent + chalk.dim(glyph + msgCell);
|
|
792
|
+
if (stringWidth(line3) > termW)
|
|
793
|
+
line3 = truncateToWidth(line3, termW);
|
|
794
|
+
lines.push(line3);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
778
797
|
return lines;
|
|
779
798
|
}
|
|
780
799
|
/** Render a single agent-session row inside an already-printed group header. */
|
|
@@ -2200,7 +2219,7 @@ export function printToolSearch(envelope) {
|
|
|
2200
2219
|
? truncate(sanitizeForTerminal(session.machine).replace(/\s+/g, ' '), 80)
|
|
2201
2220
|
: '';
|
|
2202
2221
|
const machine = machineName ? ` @ ${machineName}` : '';
|
|
2203
|
-
const rawHeading = session
|
|
2222
|
+
const rawHeading = sessionHeadline(session) || session.project || session.shortId;
|
|
2204
2223
|
const heading = truncate(sanitizeForTerminal(rawHeading).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 20));
|
|
2205
2224
|
console.log(`${chalk.cyan(session.shortId)}${chalk.gray(machine)} ${heading}`);
|
|
2206
2225
|
for (const call of session.calls) {
|
|
@@ -3374,7 +3393,7 @@ function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
|
3374
3393
|
// degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
|
|
3375
3394
|
const labelById = new Map();
|
|
3376
3395
|
for (const s of pool) {
|
|
3377
|
-
const label = s
|
|
3396
|
+
const label = sessionHeadline(s);
|
|
3378
3397
|
if (label)
|
|
3379
3398
|
labelById.set(s.id, cleanPreview(label));
|
|
3380
3399
|
}
|
|
@@ -3504,7 +3523,7 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3504
3523
|
const shown = sessionDisplayAgent(session);
|
|
3505
3524
|
const agentColor = colorAgent(shown);
|
|
3506
3525
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3507
|
-
const title = session
|
|
3526
|
+
const title = sessionHeadline(session);
|
|
3508
3527
|
console.log('');
|
|
3509
3528
|
if (title)
|
|
3510
3529
|
console.log(chalk.bold.white(title));
|
|
@@ -3562,9 +3581,10 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3562
3581
|
const modelStr = stats.models.length > 0 ? chalk.yellow(` ${stats.models.join(', ')}`) : '';
|
|
3563
3582
|
const branchStr = session.gitBranch ? chalk.gray(` (${session.gitBranch})`) : '';
|
|
3564
3583
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3565
|
-
// Auto-inferred title headline (user /rename > Claude ai-title >
|
|
3566
|
-
// topic) — the fastest way to
|
|
3567
|
-
|
|
3584
|
+
// Auto-inferred title headline (user /rename > Claude ai-title >
|
|
3585
|
+
// daemon-generated title > first-prompt topic) — the fastest way to
|
|
3586
|
+
// recognize which task this session is.
|
|
3587
|
+
const title = sessionHeadline(session);
|
|
3568
3588
|
if (title) {
|
|
3569
3589
|
const badges = signalBadges(metaSignals(session));
|
|
3570
3590
|
console.log(chalk.bold.white(title) + (badges ? ' ' + badges : ''));
|
|
@@ -4456,7 +4476,7 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
|
4456
4476
|
spinner.stop();
|
|
4457
4477
|
console.error(chalk.red(`Multiple sessions match "${query}":`));
|
|
4458
4478
|
for (const m of queryMatches.slice(0, 10)) {
|
|
4459
|
-
console.error(chalk.cyan(` ${m.shortId} ${m.id} ${m
|
|
4479
|
+
console.error(chalk.cyan(` ${m.shortId} ${m.id} ${sessionHeadline(m) ?? ''}`));
|
|
4460
4480
|
}
|
|
4461
4481
|
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
4462
4482
|
process.exit(1);
|
|
@@ -4613,7 +4633,7 @@ async function renderOneSession(query, mode, scope) {
|
|
|
4613
4633
|
spinner.stop();
|
|
4614
4634
|
console.error(chalk.red(`Multiple sessions match "${query}":`));
|
|
4615
4635
|
for (const match of queryMatches.slice(0, 10)) {
|
|
4616
|
-
console.error(chalk.cyan(` ${match.shortId} ${match.id} ${match
|
|
4636
|
+
console.error(chalk.cyan(` ${match.shortId} ${match.id} ${sessionHeadline(match) ?? ''}`));
|
|
4617
4637
|
}
|
|
4618
4638
|
console.error(chalk.gray(ambiguityHint(byId, completeId)));
|
|
4619
4639
|
process.exit(1);
|
|
@@ -5067,7 +5087,7 @@ export async function resolveSessionMetadata(selector, scope, deps = { gatherRem
|
|
|
5067
5087
|
for (const candidate of outcome.candidates) {
|
|
5068
5088
|
const session = candidate.hits[0].session;
|
|
5069
5089
|
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
5070
|
-
console.error(chalk.cyan(` ${session.shortId} ${session.id}`) + chalk.gray(` ${machines} ${session
|
|
5090
|
+
console.error(chalk.cyan(` ${session.shortId} ${session.id}`) + chalk.gray(` ${machines} ${sessionHeadline(session) ?? ''}`));
|
|
5071
5091
|
}
|
|
5072
5092
|
console.error(chalk.gray(looksLikeSessionId(selector) ? 'Pass a longer ID to narrow it down.' : 'Narrow the keywords to one session.'));
|
|
5073
5093
|
process.exit(1);
|
|
@@ -5107,7 +5127,7 @@ export async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gat
|
|
|
5107
5127
|
console.error(chalk.red(`Multiple sessions match "${query}" across the fleet:`));
|
|
5108
5128
|
for (const candidate of candidates) {
|
|
5109
5129
|
const s = candidate.hits[0].session;
|
|
5110
|
-
const label = s
|
|
5130
|
+
const label = sessionHeadline(s) ?? '';
|
|
5111
5131
|
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
5112
5132
|
console.error(chalk.cyan(` ${s.shortId} ${s.id}`) + chalk.gray(` ${machines} ${s.agent}${s.version ? ` ${s.version}` : ''} ${label}`));
|
|
5113
5133
|
}
|
package/dist/commands/utils.d.ts
CHANGED
|
@@ -45,6 +45,15 @@ export interface Surface {
|
|
|
45
45
|
* to a single `const s = resolveSurface(cmd)` without changing its flag surface.
|
|
46
46
|
*/
|
|
47
47
|
export declare function resolveSurface(cmd: Command): Surface;
|
|
48
|
+
/**
|
|
49
|
+
* Coerce a `--device` value that may arrive as a scalar or an array to a single
|
|
50
|
+
* host string. A subcommand whose own `--device` collides in name with an
|
|
51
|
+
* ancestor's variadic `-D, --device <target...>` (e.g. `sessions inject`,
|
|
52
|
+
* `sessions resume` under the parent `sessions` command) can receive an array
|
|
53
|
+
* even when the command only ever targets one device — fail loud on more than
|
|
54
|
+
* one rather than guessing the first (PHNX-3688, PHNX-3940).
|
|
55
|
+
*/
|
|
56
|
+
export declare function normalizeSingleDeviceOption(value: string | string[] | undefined, commandLabel: string): string | undefined;
|
|
48
57
|
/**
|
|
49
58
|
* Exit with a clean message when a picker would be required in a non-interactive shell.
|
|
50
59
|
*/
|
package/dist/commands/utils.js
CHANGED
|
@@ -59,6 +59,24 @@ export function resolveSurface(cmd) {
|
|
|
59
59
|
interactive: tty && !json,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Coerce a `--device` value that may arrive as a scalar or an array to a single
|
|
64
|
+
* host string. A subcommand whose own `--device` collides in name with an
|
|
65
|
+
* ancestor's variadic `-D, --device <target...>` (e.g. `sessions inject`,
|
|
66
|
+
* `sessions resume` under the parent `sessions` command) can receive an array
|
|
67
|
+
* even when the command only ever targets one device — fail loud on more than
|
|
68
|
+
* one rather than guessing the first (PHNX-3688, PHNX-3940).
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeSingleDeviceOption(value, commandLabel) {
|
|
71
|
+
const list = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
72
|
+
const hosts = list.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
73
|
+
if (hosts.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (hosts.length > 1) {
|
|
76
|
+
throw new Error(`${commandLabel} targets a single device, but --device named ${hosts.length}: ${hosts.join(', ')}.`);
|
|
77
|
+
}
|
|
78
|
+
return hosts[0];
|
|
79
|
+
}
|
|
62
80
|
/**
|
|
63
81
|
* Exit with a clean message when a picker would be required in a non-interactive shell.
|
|
64
82
|
*/
|
|
@@ -27,6 +27,7 @@ import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } f
|
|
|
27
27
|
import { loadWatchdogSessions, runWatchdogPass } from '../lib/watchdog/service.js';
|
|
28
28
|
import { readWatchdogEvents, WATCHDOG_LOG_PATH } from '../lib/watchdog/log.js';
|
|
29
29
|
import { selectWatchdogHistory } from '../lib/watchdog/history.js';
|
|
30
|
+
import { sessionHeadline } from '../lib/session/title.js';
|
|
30
31
|
/** Default state dir the runner and these subcommands share. */
|
|
31
32
|
function stateDir() {
|
|
32
33
|
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
@@ -99,7 +100,9 @@ export function formatWatchdogTickLines(result, willInject, verbose = false) {
|
|
|
99
100
|
: o.decision === 'nudge' ? 'WOULD-NUDGE'
|
|
100
101
|
: 'skip';
|
|
101
102
|
const id = o.sessionId?.slice(0, 8) ?? 'no-session-id';
|
|
102
|
-
|
|
103
|
+
// `name` is the `agents run --name` launch handle — a user-given name, so it
|
|
104
|
+
// ranks with the label; everything below it is the shared headline ladder.
|
|
105
|
+
const title = o.label || o.name || sessionHeadline(o);
|
|
103
106
|
lines.push(` ${tag.padEnd(11)} ${id}${title ? ` · ${title}` : ''}`);
|
|
104
107
|
const metadata = [
|
|
105
108
|
o.kind,
|
|
@@ -178,6 +178,14 @@ export declare function isVersionLaunchableHere(agent: AgentId, version: string)
|
|
|
178
178
|
* launched into it while the account was actually at its weekly cap.
|
|
179
179
|
*/
|
|
180
180
|
export declare const USAGE_DECISION_MAX_AGE_MS: number;
|
|
181
|
+
/**
|
|
182
|
+
* How old a *synced* snapshot (arrived from the account's poller through the
|
|
183
|
+
* fleet store) may be and still settle a routing decision. Matches the
|
|
184
|
+
* usage-sync tick so a worker that only ever sees 15-minute-old rows does not
|
|
185
|
+
* drop to the account picker. Local captures (`poll` / `statusline`) keep
|
|
186
|
+
* {@link USAGE_DECISION_MAX_AGE_MS}.
|
|
187
|
+
*/
|
|
188
|
+
export declare const USAGE_SYNC_TRUST_MS: number;
|
|
181
189
|
/**
|
|
182
190
|
* How old a usage snapshot may be before routing REFUSES to run at all
|
|
183
191
|
* (NO_VERIFIED_USAGE), as opposed to merely declining to *weight* by its number.
|
|
@@ -212,6 +220,7 @@ export declare const USAGE_STALE_REFUSAL_MAX_AGE_MS: number;
|
|
|
212
220
|
* running it refreshes that log. That self-reinforcing pin is exactly what the
|
|
213
221
|
* narrowing rule below exists to prevent.
|
|
214
222
|
*/
|
|
223
|
+
export declare function usageVerifiedMaxAgeMs(snapshot: UsageSnapshot | null | undefined): number;
|
|
215
224
|
export declare function isUsageVerified(candidate: RotateCandidate, nowMs?: number): boolean;
|
|
216
225
|
/**
|
|
217
226
|
* Whether this candidate carries a GENUINELY-STALE usage number: a snapshot with
|
|
@@ -140,6 +140,14 @@ export async function isVersionLaunchableHere(agent, version) {
|
|
|
140
140
|
* launched into it while the account was actually at its weekly cap.
|
|
141
141
|
*/
|
|
142
142
|
export const USAGE_DECISION_MAX_AGE_MS = 5 * 60 * 1000;
|
|
143
|
+
/**
|
|
144
|
+
* How old a *synced* snapshot (arrived from the account's poller through the
|
|
145
|
+
* fleet store) may be and still settle a routing decision. Matches the
|
|
146
|
+
* usage-sync tick so a worker that only ever sees 15-minute-old rows does not
|
|
147
|
+
* drop to the account picker. Local captures (`poll` / `statusline`) keep
|
|
148
|
+
* {@link USAGE_DECISION_MAX_AGE_MS}.
|
|
149
|
+
*/
|
|
150
|
+
export const USAGE_SYNC_TRUST_MS = 15 * 60 * 1000;
|
|
143
151
|
/**
|
|
144
152
|
* How old a usage snapshot may be before routing REFUSES to run at all
|
|
145
153
|
* (NO_VERIFIED_USAGE), as opposed to merely declining to *weight* by its number.
|
|
@@ -174,12 +182,18 @@ export const USAGE_STALE_REFUSAL_MAX_AGE_MS = 40 * 60 * 1000;
|
|
|
174
182
|
* running it refreshes that log. That self-reinforcing pin is exactly what the
|
|
175
183
|
* narrowing rule below exists to prevent.
|
|
176
184
|
*/
|
|
185
|
+
export function usageVerifiedMaxAgeMs(snapshot) {
|
|
186
|
+
// D8: a row that arrived via sync from the account's own poller is trusted
|
|
187
|
+
// for the sync cadence. A locally captured row (poll / statusline / unset)
|
|
188
|
+
// keeps the 5-minute bar — the poller is on this box and can refresh it.
|
|
189
|
+
return snapshot?.freshness?.source === 'sync' ? USAGE_SYNC_TRUST_MS : USAGE_DECISION_MAX_AGE_MS;
|
|
190
|
+
}
|
|
177
191
|
export function isUsageVerified(candidate, nowMs = Date.now()) {
|
|
178
192
|
const snapshot = candidate.usageSnapshot;
|
|
179
193
|
const capturedAt = snapshot?.capturedAt;
|
|
180
194
|
if (!capturedAt || !snapshot?.windows.length)
|
|
181
195
|
return false;
|
|
182
|
-
return nowMs - capturedAt.getTime() <=
|
|
196
|
+
return nowMs - capturedAt.getTime() <= usageVerifiedMaxAgeMs(snapshot);
|
|
183
197
|
}
|
|
184
198
|
/**
|
|
185
199
|
* Whether this candidate carries a GENUINELY-STALE usage number: a snapshot with
|
|
@@ -996,6 +1010,8 @@ function describeRotationCandidate(c, nowMs) {
|
|
|
996
1010
|
tier,
|
|
997
1011
|
source: snap?.source ?? null,
|
|
998
1012
|
sourceLabel: snap?.sourceLabel ?? null,
|
|
1013
|
+
captureSource: snap?.freshness?.source ?? null,
|
|
1014
|
+
pollerDevice: snap?.freshness?.poller ?? null,
|
|
999
1015
|
capturedAt: snap?.capturedAt ? snap.capturedAt.toISOString() : null,
|
|
1000
1016
|
ageMs: capturedAtMs === null ? null : nowMs - capturedAtMs,
|
|
1001
1017
|
windows: (snap?.windows ?? []).map((w) => ({ key: w.key, usedPercent: Math.round(w.usedPercent) })),
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type ConfiguredDeviceRole } from '../device-config.js';
|
|
11
11
|
import { type CachedUsageSnapshot } from './usage.js';
|
|
12
|
+
/** Cadence of the usage-sync git tick — the trust window for a `sync` snapshot. */
|
|
13
|
+
export declare const USAGE_SYNC_INTERVAL_MS: number;
|
|
14
|
+
/** Re-publish an unchanged windowed row at least this often so workers' 15-min trust does not lapse. */
|
|
15
|
+
export declare const USAGE_PUBLISH_HEARTBEAT_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* Keep the previously published `capturedAt` when meters have not moved and
|
|
18
|
+
* the last publish is still inside the heartbeat. Stops statusline re-renders
|
|
19
|
+
* from dirtying the shared store (and taking the git lock) every few seconds.
|
|
20
|
+
*/
|
|
21
|
+
export declare function mergeUsageRowsForPublish(previous: Record<string, CachedUsageSnapshot> | undefined, next: Record<string, CachedUsageSnapshot>, nowMs?: number): Record<string, CachedUsageSnapshot>;
|
|
12
22
|
/** Legacy hidden ingest/export envelope kept for older fleet CLI compatibility. */
|
|
13
23
|
export interface UsageSyncPayload {
|
|
14
24
|
v: 1;
|
|
@@ -29,6 +39,18 @@ export interface PublishUsageSnapshotResult {
|
|
|
29
39
|
}
|
|
30
40
|
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
31
41
|
export declare function publishUsageSnapshotToSharedStore(options?: PublishUsageSnapshotOptions): Promise<PublishUsageSnapshotResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Publish a changed snapshot immediately and exchange the shared repo so
|
|
44
|
+
* workers see it without waiting for the 15-minute tick. No-ops when the
|
|
45
|
+
* serialized store is unchanged (statusline re-renders with the same windows).
|
|
46
|
+
*/
|
|
47
|
+
export declare function pushUsageSnapshotNow(options?: PublishUsageSnapshotOptions & {
|
|
48
|
+
lockPath?: string;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
published: PublishUsageSnapshotResult;
|
|
52
|
+
transport?: import('../fleet-shared-repo-sync.js').FleetSharedRepoSyncResult;
|
|
53
|
+
}>;
|
|
32
54
|
export interface ConsumeUsageSnapshotsOptions {
|
|
33
55
|
userAgentsDir?: string;
|
|
34
56
|
cachePath?: string;
|
|
@@ -12,6 +12,41 @@ import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from
|
|
|
12
12
|
import { getUserAgentsDir } from '../state.js';
|
|
13
13
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
14
14
|
import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
|
|
15
|
+
/** Cadence of the usage-sync git tick — the trust window for a `sync` snapshot. */
|
|
16
|
+
export const USAGE_SYNC_INTERVAL_MS = 15 * 60_000;
|
|
17
|
+
/** Re-publish an unchanged windowed row at least this often so workers' 15-min trust does not lapse. */
|
|
18
|
+
export const USAGE_PUBLISH_HEARTBEAT_MS = 10 * 60_000;
|
|
19
|
+
function usageMeterSignature(row) {
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
windows: row.windows,
|
|
22
|
+
plan: row.plan ?? null,
|
|
23
|
+
unavailable: row.unavailable ?? null,
|
|
24
|
+
freshnessSource: row.freshnessSource ?? null,
|
|
25
|
+
pollerDevice: row.pollerDevice ?? null,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Keep the previously published `capturedAt` when meters have not moved and
|
|
30
|
+
* the last publish is still inside the heartbeat. Stops statusline re-renders
|
|
31
|
+
* from dirtying the shared store (and taking the git lock) every few seconds.
|
|
32
|
+
*/
|
|
33
|
+
export function mergeUsageRowsForPublish(previous, next, nowMs = Date.now()) {
|
|
34
|
+
if (!previous)
|
|
35
|
+
return next;
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [key, row] of Object.entries(next)) {
|
|
38
|
+
const prior = previous[key];
|
|
39
|
+
if (prior && usageMeterSignature(prior) === usageMeterSignature(row)) {
|
|
40
|
+
const priorMs = prior.capturedAt ? Date.parse(prior.capturedAt) : NaN;
|
|
41
|
+
if (Number.isFinite(priorMs) && nowMs - priorMs < USAGE_PUBLISH_HEARTBEAT_MS) {
|
|
42
|
+
out[key] = { ...row, capturedAt: prior.capturedAt };
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out[key] = row;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
15
50
|
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
16
51
|
export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
17
52
|
const result = {
|
|
@@ -26,13 +61,19 @@ export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
|
26
61
|
result.skipped = 'this device is not a usage publisher (mark it personal or desktop)';
|
|
27
62
|
return result;
|
|
28
63
|
}
|
|
29
|
-
const
|
|
30
|
-
if (Object.keys(
|
|
64
|
+
const rawRows = exportClaudeUsageCacheRows(options.cachePath);
|
|
65
|
+
if (Object.keys(rawRows).length === 0) {
|
|
31
66
|
result.skipped = 'no local usage snapshot to publish';
|
|
32
67
|
return result;
|
|
33
68
|
}
|
|
34
69
|
try {
|
|
35
|
-
const
|
|
70
|
+
const device = options.device ?? machineId();
|
|
71
|
+
const userAgentsDir = options.userAgentsDir ?? getUserAgentsDir();
|
|
72
|
+
const prior = readFleetSharedDeviceStates(userAgentsDir).states
|
|
73
|
+
.find((state) => normalizeHost(state.device) === normalizeHost(device))
|
|
74
|
+
?.usage?.rows;
|
|
75
|
+
const rows = mergeUsageRowsForPublish(prior, rawRows);
|
|
76
|
+
const write = await updateFleetSharedDeviceStateAsync(device, { usage: { rows } }, userAgentsDir);
|
|
36
77
|
result.published = true;
|
|
37
78
|
result.changed = write.changed;
|
|
38
79
|
result.path = write.path;
|
|
@@ -42,6 +83,24 @@ export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
|
42
83
|
}
|
|
43
84
|
return result;
|
|
44
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Publish a changed snapshot immediately and exchange the shared repo so
|
|
88
|
+
* workers see it without waiting for the 15-minute tick. No-ops when the
|
|
89
|
+
* serialized store is unchanged (statusline re-renders with the same windows).
|
|
90
|
+
*/
|
|
91
|
+
export async function pushUsageSnapshotNow(options = {}) {
|
|
92
|
+
const published = await publishUsageSnapshotToSharedStore(options);
|
|
93
|
+
if (!published.changed)
|
|
94
|
+
return { published };
|
|
95
|
+
const { syncFleetSharedStateRepo } = await import('../fleet-shared-repo-sync.js');
|
|
96
|
+
const transport = await syncFleetSharedStateRepo({
|
|
97
|
+
userAgentsDir: options.userAgentsDir,
|
|
98
|
+
device: options.device,
|
|
99
|
+
timeoutMs: options.timeoutMs,
|
|
100
|
+
lockPath: options.lockPath,
|
|
101
|
+
});
|
|
102
|
+
return { published, transport };
|
|
103
|
+
}
|
|
45
104
|
function capturedAtMs(row) {
|
|
46
105
|
if (!row.capturedAt)
|
|
47
106
|
return null;
|
|
@@ -70,15 +129,20 @@ export function consumeUsageSnapshotsFromSharedStore(options = {}) {
|
|
|
70
129
|
for (const [identity, incoming] of Object.entries(state.usage.rows)) {
|
|
71
130
|
if (!incoming || !Array.isArray(incoming.windows) || incoming.windows.length === 0)
|
|
72
131
|
continue;
|
|
132
|
+
const stamped = {
|
|
133
|
+
...incoming,
|
|
134
|
+
freshnessSource: 'sync',
|
|
135
|
+
pollerDevice: incoming.pollerDevice ?? state.device,
|
|
136
|
+
};
|
|
73
137
|
const current = rows[identity];
|
|
74
138
|
if (!current) {
|
|
75
|
-
rows[identity] =
|
|
139
|
+
rows[identity] = stamped;
|
|
76
140
|
continue;
|
|
77
141
|
}
|
|
78
|
-
const incomingMs = capturedAtMs(
|
|
142
|
+
const incomingMs = capturedAtMs(stamped);
|
|
79
143
|
const currentMs = capturedAtMs(current);
|
|
80
144
|
if (incomingMs !== null && (currentMs === null || incomingMs > currentMs))
|
|
81
|
-
rows[identity] =
|
|
145
|
+
rows[identity] = stamped;
|
|
82
146
|
}
|
|
83
147
|
}
|
|
84
148
|
result.sources.sort();
|
|
@@ -157,6 +157,12 @@ export declare function claudeAccessTokenNeedsRefresh(expiresAt: number | null |
|
|
|
157
157
|
export declare function setClaudeUsageCachePathForTest(cachePath: string | null): string | null;
|
|
158
158
|
/** Discriminator for usage window types. */
|
|
159
159
|
export type UsageWindowKey = 'session' | 'week' | 'sonnet_week' | 'month';
|
|
160
|
+
/**
|
|
161
|
+
* How this box obtained a usage row (delta-spec D8). Distinct from
|
|
162
|
+
* {@link UsageSnapshot.source} (`live` | `last_seen`), which is how the
|
|
163
|
+
* number was collected from the harness/API.
|
|
164
|
+
*/
|
|
165
|
+
export type UsageCaptureSource = 'poll' | 'statusline' | 'sync';
|
|
160
166
|
/** A single rate-limit window with utilization percentage and reset time. */
|
|
161
167
|
export interface UsageWindow {
|
|
162
168
|
key: UsageWindowKey;
|
|
@@ -203,6 +209,16 @@ export interface UsageSnapshot {
|
|
|
203
209
|
reason: 'session_limit' | 'out_of_credits';
|
|
204
210
|
resetsAt?: Date;
|
|
205
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* D8 freshness provenance. A `sync` row arrived from the account's poller
|
|
214
|
+
* through the fleet store and is trusted for the sync cadence; `poll` and
|
|
215
|
+
* `statusline` are local captures and keep the 5-minute decision bar.
|
|
216
|
+
*/
|
|
217
|
+
freshness?: {
|
|
218
|
+
source: UsageCaptureSource;
|
|
219
|
+
/** Device that polled or ingested the authoritative reading. */
|
|
220
|
+
poller?: string;
|
|
221
|
+
};
|
|
206
222
|
}
|
|
207
223
|
/** Usage data plus any error encountered while fetching. */
|
|
208
224
|
export interface UsageInfo {
|
|
@@ -257,6 +273,13 @@ interface UsageOptions {
|
|
|
257
273
|
* an unattended loop never transmits the interactive login to Anthropic.
|
|
258
274
|
*/
|
|
259
275
|
allowInteractiveLogin?: boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Headed usage poller: skip the setup-token and the ACL keychain, and read
|
|
278
|
+
* only `<home>/.claude/.credentials.json` (the native rotating blob). A
|
|
279
|
+
* setup-token 403s on `/api/oauth/usage`; the keychain pops Touch ID. The
|
|
280
|
+
* file blob is the Linux headed native login, and a no-op when absent.
|
|
281
|
+
*/
|
|
282
|
+
nativeFileLogin?: boolean;
|
|
260
283
|
}
|
|
261
284
|
/** Canonical input for a single usage fetch operation. */
|
|
262
285
|
export interface UsageFetchInput {
|
|
@@ -317,6 +340,10 @@ export interface CachedUsageSnapshot {
|
|
|
317
340
|
* usage bucket.
|
|
318
341
|
*/
|
|
319
342
|
modelRefusals?: Record<string, CachedModelRefusal>;
|
|
343
|
+
/** D8: `poll` | `statusline` | `sync`. Survives export → ingest. */
|
|
344
|
+
freshnessSource?: UsageCaptureSource;
|
|
345
|
+
/** D8: device that polled or ingested the authoritative reading. */
|
|
346
|
+
pollerDevice?: string;
|
|
320
347
|
}
|
|
321
348
|
/** The single registry of agent usage sources and their transport. */
|
|
322
349
|
declare const USAGE_SOURCES: {
|
|
@@ -406,6 +433,8 @@ export interface UsageLookupOptions {
|
|
|
406
433
|
* `loadClaudeOauth`. Unset for every other lookup.
|
|
407
434
|
*/
|
|
408
435
|
allowInteractiveLogin?: boolean;
|
|
436
|
+
/** Headed poller: native `.credentials.json` only, never the setup-token. */
|
|
437
|
+
nativeFileLogin?: boolean;
|
|
409
438
|
}
|
|
410
439
|
export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: UsageLookupOptions): Promise<{
|
|
411
440
|
canonicalByUsageKey: Map<string, AccountInfo>;
|
|
@@ -712,10 +741,13 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
|
|
|
712
741
|
* `.credentials.json` only. Used by the daemon usage refresher so a background
|
|
713
742
|
* tick can never pop Touch ID.
|
|
714
743
|
*/
|
|
744
|
+
/** True when `<home>/.claude/.credentials.json` is a native rotating OAuth blob. */
|
|
745
|
+
export declare function claudeHomeHasNativeOauthFile(home?: string): boolean;
|
|
715
746
|
export declare function loadClaudeOauth(home?: string, opts?: {
|
|
716
747
|
accessTokenCache?: boolean;
|
|
717
748
|
fileOnly?: boolean;
|
|
718
749
|
allowInteractiveLogin?: boolean;
|
|
750
|
+
nativeFileLogin?: boolean;
|
|
719
751
|
}): Promise<ClaudeOauthCredentials | null>;
|
|
720
752
|
/**
|
|
721
753
|
* Save Claude OAuth credentials to the system keychain/keyring.
|
|
@@ -431,6 +431,7 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
431
431
|
organizationId: input.info.organizationId,
|
|
432
432
|
fileOnly: opts?.fileOnly,
|
|
433
433
|
allowInteractiveLogin: opts?.allowInteractiveLogin,
|
|
434
|
+
nativeFileLogin: opts?.nativeFileLogin,
|
|
434
435
|
signal: opts?.signal,
|
|
435
436
|
});
|
|
436
437
|
}
|
|
@@ -469,6 +470,7 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
469
470
|
// Explicit refresh: block on the shared device collector.
|
|
470
471
|
return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true, {
|
|
471
472
|
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
473
|
+
nativeFileLogin: opts?.nativeFileLogin === true,
|
|
472
474
|
signal: opts?.signal,
|
|
473
475
|
});
|
|
474
476
|
}
|
|
@@ -501,6 +503,7 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly, opts) {
|
|
|
501
503
|
usageScope: usageKey,
|
|
502
504
|
fileOnly,
|
|
503
505
|
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
506
|
+
nativeFileLogin: opts?.nativeFileLogin === true,
|
|
504
507
|
signal: opts?.signal,
|
|
505
508
|
});
|
|
506
509
|
if (usage.snapshot) {
|
|
@@ -935,9 +938,10 @@ async function getClaudeUsageInfo(options) {
|
|
|
935
938
|
// requires (the setup-token is user:inference → 403, RUSH-2392). It is unset
|
|
936
939
|
// for every background caller, so the RUSH-1822 guarantee is untouched there.
|
|
937
940
|
const oauth = await loadClaudeOauth(options?.home, {
|
|
938
|
-
accessTokenCache: true,
|
|
939
|
-
fileOnly: options?.fileOnly === true,
|
|
941
|
+
accessTokenCache: options?.nativeFileLogin !== true,
|
|
942
|
+
fileOnly: options?.fileOnly === true || options?.nativeFileLogin === true,
|
|
940
943
|
allowInteractiveLogin: options?.allowInteractiveLogin === true,
|
|
944
|
+
nativeFileLogin: options?.nativeFileLogin === true,
|
|
941
945
|
});
|
|
942
946
|
if (!oauth?.accessToken) {
|
|
943
947
|
// NOT the shared no-credential message: "sign in" is not a remedy here.
|
|
@@ -1612,7 +1616,33 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1612
1616
|
* `.credentials.json` only. Used by the daemon usage refresher so a background
|
|
1613
1617
|
* tick can never pop Touch ID.
|
|
1614
1618
|
*/
|
|
1619
|
+
/** True when `<home>/.claude/.credentials.json` is a native rotating OAuth blob. */
|
|
1620
|
+
export function claudeHomeHasNativeOauthFile(home) {
|
|
1621
|
+
return readClaudeNativeCredentialsFile(home) !== null;
|
|
1622
|
+
}
|
|
1623
|
+
function readClaudeNativeCredentialsFile(home) {
|
|
1624
|
+
const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
|
|
1625
|
+
try {
|
|
1626
|
+
if (!fs.existsSync(credsPath))
|
|
1627
|
+
return null;
|
|
1628
|
+
const parsed = parseClaudeOauthPayload(fs.readFileSync(credsPath, 'utf-8'));
|
|
1629
|
+
// A native rotating blob has a refresh token. A setup-token-shaped file
|
|
1630
|
+
// (access only) is not a native login and must not be polled as one.
|
|
1631
|
+
if (!parsed?.accessToken?.trim() || !parsed.refreshToken?.trim())
|
|
1632
|
+
return null;
|
|
1633
|
+
return parsed;
|
|
1634
|
+
}
|
|
1635
|
+
catch {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1615
1639
|
export async function loadClaudeOauth(home, opts) {
|
|
1640
|
+
// Headed usage poller: the native rotating blob in `.credentials.json` is
|
|
1641
|
+
// the only credential that carries `user:profile` without opening the ACL
|
|
1642
|
+
// keychain (Touch ID) or firing a setup-token that 403s on /oauth/usage.
|
|
1643
|
+
if (opts?.nativeFileLogin === true) {
|
|
1644
|
+
return readClaudeNativeCredentialsFile(home);
|
|
1645
|
+
}
|
|
1616
1646
|
// Read-only usage/probe callers (accessTokenCache) authenticate ONLY with a
|
|
1617
1647
|
// file-based setup-token from the `auth` bundle — never Claude Code's
|
|
1618
1648
|
// interactive login. The usage endpoint accepts any sk-ant-oat01 bearer, and
|
|
@@ -1963,6 +1993,8 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1963
1993
|
resetsAt: window.resetsAt?.toISOString() || null,
|
|
1964
1994
|
windowMinutes: window.windowMinutes,
|
|
1965
1995
|
})),
|
|
1996
|
+
freshnessSource: snapshot.freshness?.source,
|
|
1997
|
+
pollerDevice: snapshot.freshness?.poller,
|
|
1966
1998
|
};
|
|
1967
1999
|
}
|
|
1968
2000
|
/**
|
|
@@ -2028,6 +2060,9 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
2028
2060
|
plan: snapshot.plan ?? null,
|
|
2029
2061
|
refreshHint: snapshot.refreshHint ?? null,
|
|
2030
2062
|
unavailable,
|
|
2063
|
+
freshness: snapshot.freshnessSource
|
|
2064
|
+
? { source: snapshot.freshnessSource, poller: snapshot.pollerDevice }
|
|
2065
|
+
: undefined,
|
|
2031
2066
|
};
|
|
2032
2067
|
}
|
|
2033
2068
|
/**
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -25,6 +25,8 @@ import { getCacheDir, readMeta } from './state.js';
|
|
|
25
25
|
import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, readClaudeUsageCache, } from './accounting/usage.js';
|
|
26
26
|
import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
27
27
|
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
28
|
+
import { selfConfiguredDeviceRole } from './device-config.js';
|
|
29
|
+
import { mayIssueUsageEndpointProbe, trySpendUsageApiCall } from './usage-refresh.js';
|
|
28
30
|
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
29
31
|
export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
|
|
30
32
|
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
@@ -455,6 +457,23 @@ export async function probeAuthHealth(agent, home, opts) {
|
|
|
455
457
|
if (derived)
|
|
456
458
|
return derived;
|
|
457
459
|
}
|
|
460
|
+
if (!mayIssueUsageEndpointProbe({
|
|
461
|
+
role: selfConfiguredDeviceRole(),
|
|
462
|
+
forceLive: opts?.forceLive,
|
|
463
|
+
})) {
|
|
464
|
+
return {
|
|
465
|
+
verdict: 'unverified',
|
|
466
|
+
checkedAt,
|
|
467
|
+
detail: 'setup-token box does not probe the usage endpoint',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (opts?.forceLive !== true && usageScope && !trySpendUsageApiCall(usageScope, agent, checkedAt)) {
|
|
471
|
+
return {
|
|
472
|
+
verdict: 'unverified',
|
|
473
|
+
checkedAt,
|
|
474
|
+
detail: 'usage-endpoint budget already spent this hour',
|
|
475
|
+
};
|
|
476
|
+
}
|
|
458
477
|
let probe;
|
|
459
478
|
if (agent === 'claude')
|
|
460
479
|
probe = await probeClaudeStatus(home, opts?.cliVersion, usageScope, opts?.signal);
|
|
@@ -8,6 +8,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
8
8
|
import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
9
9
|
import { loadReminders, pickReminderForSession } from './reminders.js';
|
|
10
10
|
import { readMeta } from './state.js';
|
|
11
|
+
import { machineId } from './machine-id.js';
|
|
11
12
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
12
13
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
13
14
|
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
@@ -77,7 +78,11 @@ export function ingestClaudeStatusLineUsage(payload, identity) {
|
|
|
77
78
|
sourceLabel: 'Claude response rate limits',
|
|
78
79
|
capturedAt: new Date(),
|
|
79
80
|
windows,
|
|
81
|
+
freshness: { source: 'statusline', poller: machineId() },
|
|
80
82
|
});
|
|
83
|
+
void import('./accounting/usage-sync.js')
|
|
84
|
+
.then((mod) => mod.pushUsageSnapshotNow())
|
|
85
|
+
.catch(() => { });
|
|
81
86
|
return true;
|
|
82
87
|
}
|
|
83
88
|
/**
|