@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type UsageHeadroom, type UsageSnapshot, type UsageInfo } from './accounting/usage.js';
|
|
2
2
|
import type { AgentId } from './types.js';
|
|
3
|
+
import { type ConfiguredDeviceRole } from './device-config.js';
|
|
3
4
|
/**
|
|
4
5
|
* Default schedule between successful (or attempted) live usage fetches for one
|
|
5
6
|
* account. Floor and ceiling of the delay helper are pinned to this so the
|
|
@@ -146,6 +147,48 @@ export declare function providerLastCall(accounts: LocalUsageAccount[], cache: R
|
|
|
146
147
|
* gate that binds it.
|
|
147
148
|
*/
|
|
148
149
|
export declare function providerSpacingTokens(lastCallMs: number, now: number): number;
|
|
150
|
+
/** One account considered for the headed poller. */
|
|
151
|
+
export interface UsagePollCandidate {
|
|
152
|
+
usageKey: string;
|
|
153
|
+
agentId: AgentId;
|
|
154
|
+
home: string;
|
|
155
|
+
holdsNativeLogin: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
/** Daemon auth-health may hit `/oauth/usage` only on a headed box, unless forceLive. */
|
|
159
|
+
export declare function mayIssueUsageEndpointProbe(opts: {
|
|
160
|
+
role: ConfiguredDeviceRole | undefined;
|
|
161
|
+
forceLive?: boolean;
|
|
162
|
+
}): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Sticky one-poller election: lex-least among this box (if it holds a native
|
|
165
|
+
* login) and peer devices that published a `freshnessSource=poll` row. Statusline
|
|
166
|
+
* ingest does not claim. Two headed boxes therefore converge on one poller
|
|
167
|
+
* instead of each deferring to the other.
|
|
168
|
+
*/
|
|
169
|
+
export declare function electUsagePoller(opts: {
|
|
170
|
+
selfDevice: string;
|
|
171
|
+
selfHoldsNativeLogin: boolean;
|
|
172
|
+
peerPollers?: string[];
|
|
173
|
+
}): string | null;
|
|
174
|
+
export declare function shouldPollUsageAccount(candidate: Pick<UsagePollCandidate, 'usageKey' | 'holdsNativeLogin'>, opts: {
|
|
175
|
+
role: ConfiguredDeviceRole | undefined;
|
|
176
|
+
selfDevice: string;
|
|
177
|
+
peerPollers?: string[];
|
|
178
|
+
/** @deprecated use peerPollers; kept so older tests that pass claimedBy still compile */
|
|
179
|
+
claimedBy?: Record<string, string>;
|
|
180
|
+
}): boolean;
|
|
181
|
+
/** Native rotating login on this home — Claude's `.credentials.json` blob, else a credential file. */
|
|
182
|
+
export declare function homeHoldsNativeLogin(agentId: AgentId, home: string): boolean;
|
|
183
|
+
/** usageKey → devices that published a `poll` row (statusline/sync do not claim). */
|
|
184
|
+
export declare function pollerClaimsFromSharedStore(selfDevice: string, userAgentsDir?: string): Record<string, string[]>;
|
|
185
|
+
/**
|
|
186
|
+
* Spend one live usage-endpoint call from the shared per-account / per-provider
|
|
187
|
+
* budget. Auth-health and the usage poller both go through here so they cannot
|
|
188
|
+
* together exceed {@link PROVIDER_HOURLY_BUDGET} (~30/hr, well under ~100/hr).
|
|
189
|
+
* Returns false when the call must not fire.
|
|
190
|
+
*/
|
|
191
|
+
export declare function trySpendUsageApiCall(usageKey: string, agentId: AgentId, now: number): boolean;
|
|
149
192
|
/** An account whose credentials live on the publisher host. */
|
|
150
193
|
export interface LocalUsageAccount {
|
|
151
194
|
usageKey: string;
|
|
@@ -178,15 +221,19 @@ export declare function orderUsageAccounts(accounts: LocalUsageAccount[], cache:
|
|
|
178
221
|
* have no rate-limited endpoint to budget.
|
|
179
222
|
*/
|
|
180
223
|
export declare function providerRecentCalls(accounts: LocalUsageAccount[], cache: Record<string, HeadroomEntry>, now: number): Map<AgentId, number>;
|
|
224
|
+
export interface BuildLocalUsageAccountsOpts {
|
|
225
|
+
role?: ConfiguredDeviceRole;
|
|
226
|
+
device?: string;
|
|
227
|
+
userAgentsDir?: string;
|
|
228
|
+
holdsNativeLogin?: (agentId: AgentId, home: string) => boolean;
|
|
229
|
+
claimedBy?: Record<string, string[]>;
|
|
230
|
+
}
|
|
181
231
|
/**
|
|
182
|
-
* Enumerate the usage accounts
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
186
|
-
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
187
|
-
* accounts' caches. `runUsageRefreshTick` calls this on every host.
|
|
232
|
+
* Enumerate the usage accounts THIS headed box should poll — native logins it
|
|
233
|
+
* holds, minus accounts another headed poller already claims. A worker or
|
|
234
|
+
* setup-token-only box returns [].
|
|
188
235
|
*/
|
|
189
|
-
export declare function buildLocalUsageAccounts(): Promise<LocalUsageAccount[]>;
|
|
236
|
+
export declare function buildLocalUsageAccounts(opts?: BuildLocalUsageAccountsOpts): Promise<LocalUsageAccount[]>;
|
|
190
237
|
/** Injectable side effects, so `runUsageRefresh` is drivable without the daemon. */
|
|
191
238
|
export interface UsageRefreshDeps {
|
|
192
239
|
now?: number;
|
|
@@ -212,6 +259,10 @@ export interface UsageRefreshDeps {
|
|
|
212
259
|
readCachedSnapshot?: (usageKey: string) => UsageSnapshot | null;
|
|
213
260
|
/** Daemon tick deadline signal, forwarded to each account's provider fetch (PHNX-3608). */
|
|
214
261
|
signal?: AbortSignal;
|
|
262
|
+
/** Stamp D8 provenance on a successful poll write. */
|
|
263
|
+
pollerDevice?: string;
|
|
264
|
+
/** Fire after at least one snapshot was written (push-on-change). */
|
|
265
|
+
onSnapshotsChanged?: (usageKeys: string[]) => Promise<void> | void;
|
|
215
266
|
}
|
|
216
267
|
export interface UsageRefreshResult {
|
|
217
268
|
refreshed: number;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon-owned usage refresher —
|
|
2
|
+
* Daemon-owned usage refresher — one poller per account, on a headed box.
|
|
3
3
|
*
|
|
4
4
|
* The routing hot path (`agents run` → collectRunCandidates) reads usage
|
|
5
5
|
* CACHE-ONLY (`getUsageInfoForIdentity`, RUSH-2061) and never blocks
|
|
6
|
-
* on a provider fetch.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* on a provider fetch. A headed box (`isHeadedDeviceRole`) polls only the
|
|
7
|
+
* accounts it holds native logins for; a setup-token-only box never polls.
|
|
8
|
+
* A stray `.credentials.json` for an account another headed box already
|
|
9
|
+
* publishes defers to that poller. Auth-health draws from the same
|
|
10
|
+
* per-account call budget so the two stay under the ~100/hr ceiling.
|
|
10
11
|
*
|
|
11
12
|
* Design:
|
|
12
13
|
*
|
|
@@ -48,11 +49,15 @@
|
|
|
48
49
|
*/
|
|
49
50
|
import * as fs from 'fs';
|
|
50
51
|
import * as path from 'path';
|
|
51
|
-
import { getCacheDir } from './state.js';
|
|
52
|
+
import { getCacheDir, getUserAgentsDir } from './state.js';
|
|
52
53
|
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
53
|
-
import { deriveUsageHeadroom, buildCanonicalUsageContext, agentUsesNetworkUsage, USAGE_SOURCE_AGENT_IDS, } from './accounting/usage.js';
|
|
54
|
-
import { getAccountInfo } from './agents.js';
|
|
54
|
+
import { deriveUsageHeadroom, buildCanonicalUsageContext, agentUsesNetworkUsage, USAGE_SOURCE_AGENT_IDS, claudeHomeHasNativeOauthFile, } from './accounting/usage.js';
|
|
55
|
+
import { getAccountInfo, credentialPresence } from './agents.js';
|
|
55
56
|
import { listInstalledVersions, getVersionHomePath } from './installations/versions.js';
|
|
57
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
58
|
+
import { machineId, normalizeHost } from './session/sync/config.js';
|
|
59
|
+
import { readFleetSharedDeviceStates } from './fleet-shared-state.js';
|
|
60
|
+
import { USAGE_SYNC_INTERVAL_MS } from './accounting/usage-sync.js';
|
|
56
61
|
/**
|
|
57
62
|
* Default schedule between successful (or attempted) live usage fetches for one
|
|
58
63
|
* account. Floor and ceiling of the delay helper are pinned to this so the
|
|
@@ -319,6 +324,112 @@ function failedHeadroomEntry(prev, now) {
|
|
|
319
324
|
}
|
|
320
325
|
return next;
|
|
321
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
/** Daemon auth-health may hit `/oauth/usage` only on a headed box, unless forceLive. */
|
|
329
|
+
export function mayIssueUsageEndpointProbe(opts) {
|
|
330
|
+
if (opts.forceLive === true)
|
|
331
|
+
return true;
|
|
332
|
+
return isHeadedDeviceRole(opts.role);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Sticky one-poller election: lex-least among this box (if it holds a native
|
|
336
|
+
* login) and peer devices that published a `freshnessSource=poll` row. Statusline
|
|
337
|
+
* ingest does not claim. Two headed boxes therefore converge on one poller
|
|
338
|
+
* instead of each deferring to the other.
|
|
339
|
+
*/
|
|
340
|
+
export function electUsagePoller(opts) {
|
|
341
|
+
const names = new Set();
|
|
342
|
+
if (opts.selfHoldsNativeLogin)
|
|
343
|
+
names.add(normalizeHost(opts.selfDevice));
|
|
344
|
+
for (const peer of opts.peerPollers ?? [])
|
|
345
|
+
names.add(normalizeHost(peer));
|
|
346
|
+
if (names.size === 0)
|
|
347
|
+
return null;
|
|
348
|
+
return [...names].sort()[0];
|
|
349
|
+
}
|
|
350
|
+
export function shouldPollUsageAccount(candidate, opts) {
|
|
351
|
+
if (!isHeadedDeviceRole(opts.role))
|
|
352
|
+
return false;
|
|
353
|
+
if (!candidate.holdsNativeLogin)
|
|
354
|
+
return false;
|
|
355
|
+
const peerPollers = opts.peerPollers
|
|
356
|
+
?? (opts.claimedBy?.[candidate.usageKey] ? [opts.claimedBy[candidate.usageKey]] : []);
|
|
357
|
+
const elected = electUsagePoller({
|
|
358
|
+
selfDevice: opts.selfDevice,
|
|
359
|
+
selfHoldsNativeLogin: true,
|
|
360
|
+
peerPollers,
|
|
361
|
+
});
|
|
362
|
+
return elected === normalizeHost(opts.selfDevice);
|
|
363
|
+
}
|
|
364
|
+
/** Native rotating login on this home — Claude's `.credentials.json` blob, else a credential file. */
|
|
365
|
+
export function homeHoldsNativeLogin(agentId, home) {
|
|
366
|
+
if (agentId === 'claude')
|
|
367
|
+
return claudeHomeHasNativeOauthFile(home);
|
|
368
|
+
return credentialPresence(agentId, home).perVersion;
|
|
369
|
+
}
|
|
370
|
+
/** usageKey → devices that published a `poll` row (statusline/sync do not claim). */
|
|
371
|
+
export function pollerClaimsFromSharedStore(selfDevice, userAgentsDir = getUserAgentsDir()) {
|
|
372
|
+
const claims = {};
|
|
373
|
+
const read = readFleetSharedDeviceStates(userAgentsDir);
|
|
374
|
+
const self = normalizeHost(selfDevice);
|
|
375
|
+
for (const state of read.states) {
|
|
376
|
+
if (!state.usage)
|
|
377
|
+
continue;
|
|
378
|
+
for (const [key, row] of Object.entries(state.usage.rows)) {
|
|
379
|
+
if (row.freshnessSource !== 'poll')
|
|
380
|
+
continue;
|
|
381
|
+
const capturedMs = row.capturedAt ? Date.parse(row.capturedAt) : NaN;
|
|
382
|
+
if (!Number.isFinite(capturedMs) || Date.now() - capturedMs > USAGE_SYNC_INTERVAL_MS)
|
|
383
|
+
continue;
|
|
384
|
+
const poller = normalizeHost(row.pollerDevice ?? state.device);
|
|
385
|
+
if (!poller || poller === self)
|
|
386
|
+
continue;
|
|
387
|
+
const list = claims[key] ?? [];
|
|
388
|
+
if (!list.includes(poller))
|
|
389
|
+
list.push(poller);
|
|
390
|
+
claims[key] = list;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return claims;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Spend one live usage-endpoint call from the shared per-account / per-provider
|
|
397
|
+
* budget. Auth-health and the usage poller both go through here so they cannot
|
|
398
|
+
* together exceed {@link PROVIDER_HOURLY_BUDGET} (~30/hr, well under ~100/hr).
|
|
399
|
+
* Returns false when the call must not fire.
|
|
400
|
+
*/
|
|
401
|
+
export function trySpendUsageApiCall(usageKey, agentId, now) {
|
|
402
|
+
if (!agentUsesNetworkUsage(agentId))
|
|
403
|
+
return true;
|
|
404
|
+
const cache = readHeadroomCache();
|
|
405
|
+
const entry = cache[usageKey];
|
|
406
|
+
const recent = pruneCallTimestamps(entry?.callTimestamps ?? [], now);
|
|
407
|
+
if (recent.length >= HOURLY_CALL_CAP)
|
|
408
|
+
return false;
|
|
409
|
+
let providerSpent = recent.length;
|
|
410
|
+
for (const [key, other] of Object.entries(cache)) {
|
|
411
|
+
if (key === usageKey)
|
|
412
|
+
continue;
|
|
413
|
+
if (!key.startsWith(`${agentId}:`))
|
|
414
|
+
continue;
|
|
415
|
+
providerSpent += pruneCallTimestamps(other.callTimestamps ?? [], now).length;
|
|
416
|
+
}
|
|
417
|
+
if (providerSpent >= PROVIDER_HOURLY_BUDGET)
|
|
418
|
+
return false;
|
|
419
|
+
writeHeadroomEntries({
|
|
420
|
+
[usageKey]: {
|
|
421
|
+
status: entry?.status ?? null,
|
|
422
|
+
minutesToLimit: entry?.minutesToLimit ?? null,
|
|
423
|
+
sessionUsedPercent: entry?.sessionUsedPercent ?? null,
|
|
424
|
+
capturedAt: entry?.capturedAt ?? null,
|
|
425
|
+
nextRefreshAt: entry?.nextRefreshAt ?? now,
|
|
426
|
+
callTimestamps: [...recent, now],
|
|
427
|
+
computedAt: now,
|
|
428
|
+
consecutiveFailures: entry?.consecutiveFailures ?? 0,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
322
433
|
/**
|
|
323
434
|
* Order a pass STALEST-FIRST so a scarce per-provider budget
|
|
324
435
|
* ({@link PROVIDER_HOURLY_BUDGET}) is always spent on the accounts most in need
|
|
@@ -362,14 +473,17 @@ export function providerRecentCalls(accounts, cache, now) {
|
|
|
362
473
|
return counts;
|
|
363
474
|
}
|
|
364
475
|
/**
|
|
365
|
-
* Enumerate the usage accounts
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
369
|
-
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
370
|
-
* accounts' caches. `runUsageRefreshTick` calls this on every host.
|
|
476
|
+
* Enumerate the usage accounts THIS headed box should poll — native logins it
|
|
477
|
+
* holds, minus accounts another headed poller already claims. A worker or
|
|
478
|
+
* setup-token-only box returns [].
|
|
371
479
|
*/
|
|
372
|
-
export async function buildLocalUsageAccounts() {
|
|
480
|
+
export async function buildLocalUsageAccounts(opts = {}) {
|
|
481
|
+
const role = opts.role ?? selfConfiguredDeviceRole();
|
|
482
|
+
if (!isHeadedDeviceRole(role))
|
|
483
|
+
return [];
|
|
484
|
+
const selfDevice = opts.device ?? machineId();
|
|
485
|
+
const claimedBy = opts.claimedBy ?? pollerClaimsFromSharedStore(selfDevice, opts.userAgentsDir);
|
|
486
|
+
const nativeAt = opts.holdsNativeLogin ?? homeHoldsNativeLogin;
|
|
373
487
|
const accounts = [];
|
|
374
488
|
for (const agentId of USAGE_SOURCE_AGENT_IDS) {
|
|
375
489
|
const versions = listInstalledVersions(agentId);
|
|
@@ -384,14 +498,15 @@ export async function buildLocalUsageAccounts() {
|
|
|
384
498
|
const canonical = canonicalByUsageKey.get(usageKey);
|
|
385
499
|
if (!canonical?.signedIn)
|
|
386
500
|
continue; // only refresh accounts actually usable here
|
|
501
|
+
const home = fetchInput.home ?? getVersionHomePath(agentId, fetchInput.cliVersion ?? '');
|
|
502
|
+
if (!shouldPollUsageAccount({ usageKey, holdsNativeLogin: nativeAt(agentId, home) }, { role, selfDevice, peerPollers: claimedBy[usageKey] }))
|
|
503
|
+
continue;
|
|
387
504
|
accounts.push({
|
|
388
505
|
usageKey,
|
|
389
506
|
agentId,
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// reads as "usage unavailable (no usage credential)" — "usage pending"
|
|
394
|
-
// is now the cold-cache state only (#2987).
|
|
507
|
+
// Native file login: skip setup-token (403s on /oauth/usage) and the
|
|
508
|
+
// ACL keychain (Touch ID). Linux headed boxes store the rotating blob
|
|
509
|
+
// in `.credentials.json`; a missing file is a no-op fetch.
|
|
395
510
|
fetch: async (signal) => {
|
|
396
511
|
const { getUsageInfoForIdentity } = await import('./accounting/usage.js');
|
|
397
512
|
return getUsageInfoForIdentity({
|
|
@@ -399,7 +514,7 @@ export async function buildLocalUsageAccounts() {
|
|
|
399
514
|
home: fetchInput.home,
|
|
400
515
|
cliVersion: fetchInput.cliVersion,
|
|
401
516
|
info: canonical,
|
|
402
|
-
}, { forceRefresh: true, fileOnly: true, signal });
|
|
517
|
+
}, { forceRefresh: true, fileOnly: true, nativeFileLogin: true, signal });
|
|
403
518
|
},
|
|
404
519
|
});
|
|
405
520
|
}
|
|
@@ -441,6 +556,7 @@ export async function runUsageRefresh(deps) {
|
|
|
441
556
|
spacingTokens.set(agent, providerSpacingTokens(last, now));
|
|
442
557
|
const spacingUsed = new Map();
|
|
443
558
|
const updates = {};
|
|
559
|
+
const refreshedKeys = [];
|
|
444
560
|
for (const [index, account] of accounts.entries()) {
|
|
445
561
|
const entry = cache[account.usageKey] ?? null;
|
|
446
562
|
const network = agentUsesNetworkUsage(account.agentId);
|
|
@@ -492,9 +608,13 @@ export async function runUsageRefresh(deps) {
|
|
|
492
608
|
// `source` is provenance, not freshness. A forced collection that just
|
|
493
609
|
// reread a local harness event returns `last_seen`; that is still a
|
|
494
610
|
// successful collection and belongs in the shared read cache.
|
|
495
|
-
deps.
|
|
496
|
-
|
|
611
|
+
const stamped = deps.pollerDevice
|
|
612
|
+
? { ...usage.snapshot, freshness: { source: 'poll', poller: deps.pollerDevice } }
|
|
613
|
+
: usage.snapshot;
|
|
614
|
+
deps.writeUsageCache(account.usageKey, stamped);
|
|
615
|
+
updates[account.usageKey] = nextHeadroomEntry(entry, stamped, now);
|
|
497
616
|
result.refreshed += 1;
|
|
617
|
+
refreshedKeys.push(account.usageKey);
|
|
498
618
|
}
|
|
499
619
|
else {
|
|
500
620
|
// No live snapshot (expired token / fetch miss): don't rewrite the usage
|
|
@@ -511,5 +631,7 @@ export async function runUsageRefresh(deps) {
|
|
|
511
631
|
}
|
|
512
632
|
if (Object.keys(updates).length > 0)
|
|
513
633
|
writeHeadroomEntries(updates);
|
|
634
|
+
if (refreshedKeys.length > 0)
|
|
635
|
+
await deps.onSnapshotsChanged?.(refreshedKeys);
|
|
514
636
|
return result;
|
|
515
637
|
}
|
|
@@ -142,6 +142,9 @@ export interface SessionOutcome {
|
|
|
142
142
|
project?: string | null;
|
|
143
143
|
label?: string;
|
|
144
144
|
name?: string;
|
|
145
|
+
/** The daemon-generated headline (PHNX-3797); carried so the report renders the
|
|
146
|
+
* same name every other surface shows, not the raw first prompt. */
|
|
147
|
+
generatedTitle?: string;
|
|
145
148
|
topic?: string;
|
|
146
149
|
preview?: string;
|
|
147
150
|
activity?: ActiveSession['activity'];
|
|
@@ -444,6 +444,7 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
444
444
|
project: session.project,
|
|
445
445
|
label: session.label,
|
|
446
446
|
name: session.name,
|
|
447
|
+
generatedTitle: session.generatedTitle,
|
|
447
448
|
topic: session.topic,
|
|
448
449
|
preview: session.preview,
|
|
449
450
|
activity: session.activity,
|
|
@@ -17,11 +17,11 @@ function hookCommand(agent, opts) {
|
|
|
17
17
|
const hook = opts.hookPathOverride ?? HOOK_PATH;
|
|
18
18
|
return `${hook} ${agent}`;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
* `hook.sh` (src or dist). Used for idempotency when stripping old entries. */
|
|
20
|
+
/** Recognize current registrations and the retired builtin sidecar writer. */
|
|
22
21
|
function isOwnHookCommand(command) {
|
|
23
22
|
const first = command.trim().split(/\s+/)[0];
|
|
24
|
-
return first.endsWith('hook.sh') && first.includes('session-tracker')
|
|
23
|
+
return (first.endsWith('hook.sh') && first.includes('session-tracker'))
|
|
24
|
+
|| first.endsWith('/.agents/.cache/shims/builtin-hooks/session-tracker.sh');
|
|
25
25
|
}
|
|
26
26
|
async function readJson(p) {
|
|
27
27
|
try {
|
|
@@ -131,7 +131,7 @@ async function installDroid(opts) {
|
|
|
131
131
|
for (const entry of cfg.hooks.SessionStart) {
|
|
132
132
|
if (!entry || !Array.isArray(entry.hooks))
|
|
133
133
|
continue;
|
|
134
|
-
entry.hooks = entry.hooks.filter((hook) => !(hook?.command && String(hook.command)
|
|
134
|
+
entry.hooks = entry.hooks.filter((hook) => !(hook?.command && isOwnHookCommand(String(hook.command))));
|
|
135
135
|
}
|
|
136
136
|
let group = cfg.hooks.SessionStart.find((entry) => entry?.matcher === '');
|
|
137
137
|
if (!group) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.111",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|