@phnx-labs/agents-cli 1.22.66 → 1.22.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -318,6 +318,16 @@ export async function getUsageInfo(agentId, options) {
|
|
|
318
318
|
const source = getUsageSource(agentId);
|
|
319
319
|
return source ? source.fetch(options) : { snapshot: null, error: null };
|
|
320
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Combine a caller-supplied abort signal (the daemon tick deadline) with a
|
|
323
|
+
* per-fetch timeout, so a provider fetch is bounded by whichever fires first
|
|
324
|
+
* (PHNX-3608). With no caller signal it degrades to the timeout alone —
|
|
325
|
+
* byte-identical to the previous `AbortSignal.timeout(ms)` behaviour.
|
|
326
|
+
*/
|
|
327
|
+
function usageFetchSignal(signal, timeoutMs) {
|
|
328
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
329
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
330
|
+
}
|
|
321
331
|
/** Derive a stable lookup key from account info for usage deduplication. */
|
|
322
332
|
export function getUsageLookupKey(info) {
|
|
323
333
|
return info?.usageKey || info?.accountKey || null;
|
|
@@ -421,6 +431,7 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
421
431
|
organizationId: input.info.organizationId,
|
|
422
432
|
fileOnly: opts?.fileOnly,
|
|
423
433
|
allowInteractiveLogin: opts?.allowInteractiveLogin,
|
|
434
|
+
signal: opts?.signal,
|
|
424
435
|
});
|
|
425
436
|
}
|
|
426
437
|
const cached = readClaudeUsageCache(usageKey);
|
|
@@ -458,6 +469,7 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
458
469
|
// Explicit refresh: block on the shared device collector.
|
|
459
470
|
return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true, {
|
|
460
471
|
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
472
|
+
signal: opts?.signal,
|
|
461
473
|
});
|
|
462
474
|
}
|
|
463
475
|
/**
|
|
@@ -489,6 +501,7 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly, opts) {
|
|
|
489
501
|
usageScope: usageKey,
|
|
490
502
|
fileOnly,
|
|
491
503
|
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
504
|
+
signal: opts?.signal,
|
|
492
505
|
});
|
|
493
506
|
if (usage.snapshot) {
|
|
494
507
|
if (!usage.snapshot.capturedAt || usage.snapshot.capturedAt.getTime() <= previousCapturedAt) {
|
|
@@ -931,7 +944,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
931
944
|
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
|
932
945
|
'User-Agent': getClaudeUserAgent(options?.cliVersion),
|
|
933
946
|
},
|
|
934
|
-
signal:
|
|
947
|
+
signal: usageFetchSignal(options?.signal, 5000),
|
|
935
948
|
});
|
|
936
949
|
if (!response.ok) {
|
|
937
950
|
if (response.status === 429) {
|
|
@@ -1038,7 +1051,7 @@ async function getKimiUsageInfo(options) {
|
|
|
1038
1051
|
Authorization: `Bearer ${accessToken}`,
|
|
1039
1052
|
Accept: 'application/json',
|
|
1040
1053
|
},
|
|
1041
|
-
signal:
|
|
1054
|
+
signal: usageFetchSignal(options?.signal, 5000),
|
|
1042
1055
|
});
|
|
1043
1056
|
// 401/403 => expired token, 404 => no Kimi For Coding subscription. Either
|
|
1044
1057
|
// way there are no bars to draw, and the status is what tells them apart.
|
|
@@ -1172,7 +1185,7 @@ async function getDroidUsageInfo(options) {
|
|
|
1172
1185
|
Authorization: `Bearer ${accessToken}`,
|
|
1173
1186
|
Accept: 'application/json',
|
|
1174
1187
|
},
|
|
1175
|
-
signal:
|
|
1188
|
+
signal: usageFetchSignal(options?.signal, 5000),
|
|
1176
1189
|
});
|
|
1177
1190
|
// 401 => revoked/expired token. No bars to draw, and the status says why.
|
|
1178
1191
|
if (!response.ok) {
|
|
@@ -1204,7 +1217,7 @@ async function getDroidUsageInfo(options) {
|
|
|
1204
1217
|
}
|
|
1205
1218
|
}
|
|
1206
1219
|
/** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
|
|
1207
|
-
export async function probeClaudeStatus(home, cliVersion, usageScope) {
|
|
1220
|
+
export async function probeClaudeStatus(home, cliVersion, usageScope, signal) {
|
|
1208
1221
|
// accessTokenCache: the daemon warms this probe every ~3 min per account, so it
|
|
1209
1222
|
// reads ONLY the file-based setup-token and never the interactive login —
|
|
1210
1223
|
// transmitting that ACL-bound token to the usage API from a background loop is
|
|
@@ -1240,7 +1253,7 @@ export async function probeClaudeStatus(home, cliVersion, usageScope) {
|
|
|
1240
1253
|
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
|
1241
1254
|
'User-Agent': getClaudeUserAgent(cliVersion),
|
|
1242
1255
|
},
|
|
1243
|
-
signal:
|
|
1256
|
+
signal: usageFetchSignal(signal, 8000),
|
|
1244
1257
|
});
|
|
1245
1258
|
if (response.status === 429) {
|
|
1246
1259
|
noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: usageScope });
|
|
@@ -1271,7 +1284,7 @@ export async function probeClaudeStatus(home, cliVersion, usageScope) {
|
|
|
1271
1284
|
}
|
|
1272
1285
|
}
|
|
1273
1286
|
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
1274
|
-
export async function probeKimiStatus(home, usageScope) {
|
|
1287
|
+
export async function probeKimiStatus(home, usageScope, signal) {
|
|
1275
1288
|
const credPath = resolveKimiCredentialPath(home);
|
|
1276
1289
|
if (!credPath)
|
|
1277
1290
|
return { status: null, token: 'missing' };
|
|
@@ -1301,7 +1314,7 @@ export async function probeKimiStatus(home, usageScope) {
|
|
|
1301
1314
|
const response = await fetch(KIMI_USAGES_URL, {
|
|
1302
1315
|
method: 'GET',
|
|
1303
1316
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
1304
|
-
signal:
|
|
1317
|
+
signal: usageFetchSignal(signal, 8000),
|
|
1305
1318
|
});
|
|
1306
1319
|
if (response.status === 429) {
|
|
1307
1320
|
noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: usageScope });
|
|
@@ -1313,7 +1326,7 @@ export async function probeKimiStatus(home, usageScope) {
|
|
|
1313
1326
|
}
|
|
1314
1327
|
}
|
|
1315
1328
|
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
1316
|
-
export async function probeDroidStatus(home, usageScope) {
|
|
1329
|
+
export async function probeDroidStatus(home, usageScope, signal) {
|
|
1317
1330
|
const cred = decryptDroidAuthPayload(home || os.homedir());
|
|
1318
1331
|
const accessToken = cred?.access_token;
|
|
1319
1332
|
if (typeof accessToken !== 'string' || !accessToken)
|
|
@@ -1331,7 +1344,7 @@ export async function probeDroidStatus(home, usageScope) {
|
|
|
1331
1344
|
const response = await fetch(DROID_USAGE_URL, {
|
|
1332
1345
|
method: 'GET',
|
|
1333
1346
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
1334
|
-
signal:
|
|
1347
|
+
signal: usageFetchSignal(signal, 8000),
|
|
1335
1348
|
});
|
|
1336
1349
|
if (response.status === 429) {
|
|
1337
1350
|
noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: usageScope });
|
|
@@ -1887,6 +1900,17 @@ function writeClaudeUsageCacheFile(cache, cachePath) {
|
|
|
1887
1900
|
}
|
|
1888
1901
|
/** Convert a live UsageSnapshot to its JSON-serializable cached form. */
|
|
1889
1902
|
function serializeClaudeUsageSnapshot(snapshot) {
|
|
1903
|
+
// Persist the union of fresh `windows` and last-known `staleWindows`.
|
|
1904
|
+
// `deserializeClaudeUsageSnapshot` re-runs the freshness gate on read and
|
|
1905
|
+
// re-partitions the serialized windows into fresh vs. stale, so what matters
|
|
1906
|
+
// is that every last-known reading reaches disk. Claude's collector returns
|
|
1907
|
+
// raw windows (no `staleWindows`) and relies on that read-side partition. But
|
|
1908
|
+
// Grok's collector pre-partitions in the fetch, moving an ended-period reading
|
|
1909
|
+
// onto `staleWindows` — serializing only `windows` dropped it, so the very
|
|
1910
|
+
// number a daemon `--refresh` just captured was gone from the next cached
|
|
1911
|
+
// `agents view grok`, which rendered the plan alone (no bar). Include the
|
|
1912
|
+
// stale windows here so the round-trip preserves them for any collector.
|
|
1913
|
+
const persistedWindows = [...snapshot.windows, ...(snapshot.staleWindows ?? [])];
|
|
1890
1914
|
return {
|
|
1891
1915
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1892
1916
|
plan: snapshot.plan ?? null,
|
|
@@ -1897,7 +1921,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1897
1921
|
resetsAt: snapshot.unavailable.resetsAt?.toISOString(),
|
|
1898
1922
|
}
|
|
1899
1923
|
: undefined,
|
|
1900
|
-
windows:
|
|
1924
|
+
windows: persistedWindows.map((window) => ({
|
|
1901
1925
|
key: window.key,
|
|
1902
1926
|
label: window.label,
|
|
1903
1927
|
shortLabel: window.shortLabel,
|
|
@@ -2387,17 +2411,131 @@ function safeStatSync(filePath) {
|
|
|
2387
2411
|
return null;
|
|
2388
2412
|
}
|
|
2389
2413
|
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Resolve the Grok billing log to read usage from.
|
|
2416
|
+
*
|
|
2417
|
+
* `agents view grok` reads usage per INSTALLED VERSION, passing each version's
|
|
2418
|
+
* isolated home (`~/.agents/.history/versions/grok/<ver>`). Grok writes
|
|
2419
|
+
* `unified.jsonl` only to the shared real home `~/.grok/logs/unified.jsonl`
|
|
2420
|
+
* even though GROK_HOME isolates auth/config/models per version. A per-version
|
|
2421
|
+
* log, if one ever appears, still wins; otherwise we return the shared path
|
|
2422
|
+
* marked `shared: true` so the caller can attribute it to at most one identity.
|
|
2423
|
+
* Grok accounts are version-scoped (`NATIVE_ACCOUNT_CAPABILITIES.grok.scope ===
|
|
2424
|
+
* 'version'`) — the shared file has no owner, so it must not be stamped onto
|
|
2425
|
+
* every version home.
|
|
2426
|
+
*/
|
|
2427
|
+
function resolveGrokBillingLogPath(home) {
|
|
2428
|
+
const rel = ['.grok', 'logs', 'unified.jsonl'];
|
|
2429
|
+
const perVersion = path.join(home || os.homedir(), ...rel);
|
|
2430
|
+
try {
|
|
2431
|
+
if (fs.existsSync(perVersion))
|
|
2432
|
+
return { logPath: perVersion, shared: false };
|
|
2433
|
+
}
|
|
2434
|
+
catch { /* unreadable */ }
|
|
2435
|
+
// `AGENTS_REAL_HOME` is the seam every version-home consumer honors: a
|
|
2436
|
+
// daemon/service-manager child's HOME can be baked to something other than
|
|
2437
|
+
// the account's real home, so os.homedir() alone is not a reliable stand-in.
|
|
2438
|
+
const shared = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...rel);
|
|
2439
|
+
if (shared !== perVersion) {
|
|
2440
|
+
try {
|
|
2441
|
+
if (fs.existsSync(shared))
|
|
2442
|
+
return { logPath: shared, shared: true };
|
|
2443
|
+
}
|
|
2444
|
+
catch { /* unreadable */ }
|
|
2445
|
+
}
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
/** This home's own `.grok/auth.json` only — never the shared-home fallback. */
|
|
2449
|
+
function readGrokAuthIdentity(home) {
|
|
2450
|
+
const authPath = path.join(home, '.grok', 'auth.json');
|
|
2451
|
+
try {
|
|
2452
|
+
if (!fs.existsSync(authPath))
|
|
2453
|
+
return null;
|
|
2454
|
+
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
2455
|
+
const records = (data && typeof data === 'object' ? [data, ...Object.values(data)] : [])
|
|
2456
|
+
.filter((r) => !!r && typeof r === 'object' && !Array.isArray(r));
|
|
2457
|
+
const account = records
|
|
2458
|
+
.filter((r) => typeof r.refresh_token === 'string' || typeof r.email === 'string' || typeof r.user_id === 'string')
|
|
2459
|
+
.sort((a, b) => String(b.create_time || '').localeCompare(String(a.create_time || '')))[0];
|
|
2460
|
+
if (!account)
|
|
2461
|
+
return null;
|
|
2462
|
+
const userRaw = account.user_id ?? account.principal_id;
|
|
2463
|
+
const userId = typeof userRaw === 'string' && userRaw.trim() ? userRaw.trim() : null;
|
|
2464
|
+
const email = typeof account.email === 'string' && account.email.trim()
|
|
2465
|
+
? account.email.trim().toLowerCase()
|
|
2466
|
+
: null;
|
|
2467
|
+
if (!userId && !email)
|
|
2468
|
+
return null;
|
|
2469
|
+
return { userId, email };
|
|
2470
|
+
}
|
|
2471
|
+
catch {
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function grokIdentitiesMatch(a, b) {
|
|
2476
|
+
if (!a || !b)
|
|
2477
|
+
return false;
|
|
2478
|
+
if (a.userId && b.userId)
|
|
2479
|
+
return a.userId === b.userId;
|
|
2480
|
+
if (a.email && b.email)
|
|
2481
|
+
return a.email === b.email;
|
|
2482
|
+
return false;
|
|
2483
|
+
}
|
|
2484
|
+
function grokIdentityFromBillingPayload(parsed) {
|
|
2485
|
+
const ctx = parsed.ctx && typeof parsed.ctx === 'object' && !Array.isArray(parsed.ctx)
|
|
2486
|
+
? parsed.ctx
|
|
2487
|
+
: null;
|
|
2488
|
+
const config = ctx?.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)
|
|
2489
|
+
? ctx.config
|
|
2490
|
+
: null;
|
|
2491
|
+
const pick = (...cands) => {
|
|
2492
|
+
for (const c of cands) {
|
|
2493
|
+
if (typeof c === 'string' && c.trim())
|
|
2494
|
+
return c.trim();
|
|
2495
|
+
}
|
|
2496
|
+
return null;
|
|
2497
|
+
};
|
|
2498
|
+
const userId = pick(ctx?.user_id, ctx?.userId, ctx?.principal_id, config?.user_id, parsed.user_id);
|
|
2499
|
+
const emailRaw = pick(ctx?.email, config?.email, parsed.email);
|
|
2500
|
+
if (!userId && !emailRaw)
|
|
2501
|
+
return null;
|
|
2502
|
+
return { userId, email: emailRaw ? emailRaw.toLowerCase() : null };
|
|
2503
|
+
}
|
|
2504
|
+
function sameHomePath(a, b) {
|
|
2505
|
+
return (safeRealpathSync(a) ?? path.resolve(a)) === (safeRealpathSync(b) ?? path.resolve(b));
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Whether the shared `~/.grok` billing log may be attached to this home.
|
|
2509
|
+
* Grok logins are per version home; the shared last line is one account's
|
|
2510
|
+
* meter. Fail loud: never copy it onto every installed identity.
|
|
2511
|
+
*/
|
|
2512
|
+
function sharedGrokLogAppliesToHome(home, match) {
|
|
2513
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
2514
|
+
const requestedHome = home || os.homedir();
|
|
2515
|
+
const thisId = readGrokAuthIdentity(requestedHome);
|
|
2516
|
+
if (match.identity) {
|
|
2517
|
+
return grokIdentitiesMatch(match.identity, thisId);
|
|
2518
|
+
}
|
|
2519
|
+
// No identity on the line (the live Grok shape). Attribute the reading to
|
|
2520
|
+
// exactly one canonical identity: the real home itself, or the version home
|
|
2521
|
+
// whose own auth.json matches the shared `~/.grok/auth.json`.
|
|
2522
|
+
if (sameHomePath(requestedHome, realHome))
|
|
2523
|
+
return true;
|
|
2524
|
+
return grokIdentitiesMatch(thisId, readGrokAuthIdentity(realHome));
|
|
2525
|
+
}
|
|
2390
2526
|
/** Parse the latest billing info from Grok's unified log. */
|
|
2391
2527
|
async function getGrokUsageInfo(options) {
|
|
2392
2528
|
try {
|
|
2393
|
-
const
|
|
2394
|
-
const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
|
|
2529
|
+
const resolved = resolveGrokBillingLogPath(options?.home);
|
|
2395
2530
|
// No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
|
|
2396
|
-
if (!
|
|
2531
|
+
if (!resolved)
|
|
2397
2532
|
return usageNoRecentUsageInfo();
|
|
2398
|
-
const match = await readLatestGrokBilling(logPath);
|
|
2533
|
+
const match = await readLatestGrokBilling(resolved.logPath);
|
|
2399
2534
|
if (!match)
|
|
2400
2535
|
return usageNoRecentUsageInfo();
|
|
2536
|
+
if (resolved.shared && !sharedGrokLogAppliesToHome(options?.home, match)) {
|
|
2537
|
+
return usageNoRecentUsageInfo();
|
|
2538
|
+
}
|
|
2401
2539
|
// Grok has no live usage API (`network: false`) — bars are last-seen from
|
|
2402
2540
|
// this machine's unified.jsonl only. Drop windows whose billing period has
|
|
2403
2541
|
// already ended so a stale 100% does not paint "rate-limited" after reset,
|
|
@@ -2690,10 +2828,16 @@ async function readLatestGrokBilling(filePath) {
|
|
|
2690
2828
|
return;
|
|
2691
2829
|
try {
|
|
2692
2830
|
const parsed = JSON.parse(line);
|
|
2693
|
-
|
|
2694
|
-
|
|
2831
|
+
const ctx = parsed.ctx && typeof parsed.ctx === 'object' && !Array.isArray(parsed.ctx)
|
|
2832
|
+
? parsed.ctx
|
|
2833
|
+
: null;
|
|
2834
|
+
if (parsed.msg === 'billing: fetched credits config' && ctx?.config) {
|
|
2835
|
+
const config = ctx.config;
|
|
2695
2836
|
const windows = [];
|
|
2696
|
-
|
|
2837
|
+
const currentPeriod = config.currentPeriod && typeof config.currentPeriod === 'object'
|
|
2838
|
+
? config.currentPeriod
|
|
2839
|
+
: null;
|
|
2840
|
+
if (currentPeriod?.end && typeof config.creditUsagePercent === 'number') {
|
|
2697
2841
|
// `creditUsagePercent` is Grok's weekly credit consumption (0-100);
|
|
2698
2842
|
// the billing period's `end` is when that window resets.
|
|
2699
2843
|
// Do NOT coerce a missing percent to 0 — a new period often lands a
|
|
@@ -2705,14 +2849,15 @@ async function readLatestGrokBilling(filePath) {
|
|
|
2705
2849
|
label: 'Current week',
|
|
2706
2850
|
shortLabel: 'W',
|
|
2707
2851
|
usedPercent: Math.max(0, Math.min(100, rawPercent)),
|
|
2708
|
-
resetsAt: parseDateValue(
|
|
2852
|
+
resetsAt: parseDateValue(currentPeriod.end),
|
|
2709
2853
|
windowMinutes: inferWindowMinutes('week'),
|
|
2710
2854
|
});
|
|
2711
2855
|
}
|
|
2712
2856
|
latest = {
|
|
2713
2857
|
capturedAt: parseDateValue(parsed.ts),
|
|
2714
|
-
subscriptionTier:
|
|
2858
|
+
subscriptionTier: typeof ctx.subscriptionTier === 'string' ? ctx.subscriptionTier : null,
|
|
2715
2859
|
windows,
|
|
2860
|
+
identity: grokIdentityFromBillingPayload(parsed),
|
|
2716
2861
|
};
|
|
2717
2862
|
}
|
|
2718
2863
|
}
|
|
@@ -2963,7 +3108,7 @@ async function getCursorUsageInfo(options) {
|
|
|
2963
3108
|
Cookie: cookie,
|
|
2964
3109
|
Accept: 'application/json',
|
|
2965
3110
|
},
|
|
2966
|
-
signal:
|
|
3111
|
+
signal: usageFetchSignal(options?.signal, 5000),
|
|
2967
3112
|
});
|
|
2968
3113
|
// 401/redirect => revoked/expired session. No bars to draw, and the status
|
|
2969
3114
|
// says why.
|
|
@@ -163,6 +163,8 @@ export declare function probeAuthHealth(agent: AgentId, home: string | undefined
|
|
|
163
163
|
* a genuinely live request that surfaces `revoked` immediately.
|
|
164
164
|
*/
|
|
165
165
|
forceLive?: boolean;
|
|
166
|
+
/** Daemon tick deadline signal, combined with each probe fetch's own timeout (PHNX-3608). */
|
|
167
|
+
signal?: AbortSignal;
|
|
166
168
|
}): Promise<AuthHealth>;
|
|
167
169
|
/** One probed install on a host. */
|
|
168
170
|
export interface AuthProbeRow {
|
|
@@ -225,6 +227,12 @@ export declare function probeLocalFleetAuth(opts?: {
|
|
|
225
227
|
agents?: readonly AgentId[];
|
|
226
228
|
/** Fire real network probes even when fresh usage evidence exists (RUSH-3036) — the `devices ping [--strict]` contract. */
|
|
227
229
|
forceLive?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Deadline signal from the daemon's supervised auth tick (PHNX-3608). Aborts
|
|
232
|
+
* the probe's in-flight network work when the tick deadline elapses so it
|
|
233
|
+
* unwinds instead of leaking a runaway await; on-demand CLI callers omit it.
|
|
234
|
+
*/
|
|
235
|
+
signal?: AbortSignal;
|
|
228
236
|
}): Promise<AuthProbeRow[]>;
|
|
229
237
|
/** Persist a host's probed rows into the cache (keyed by host+agent+version). */
|
|
230
238
|
export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[]): void;
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -347,11 +347,11 @@ export async function probeAuthHealth(agent, home, opts) {
|
|
|
347
347
|
}
|
|
348
348
|
let probe;
|
|
349
349
|
if (agent === 'claude')
|
|
350
|
-
probe = await probeClaudeStatus(home, opts?.cliVersion, usageScope);
|
|
350
|
+
probe = await probeClaudeStatus(home, opts?.cliVersion, usageScope, opts?.signal);
|
|
351
351
|
else if (agent === 'kimi')
|
|
352
|
-
probe = await probeKimiStatus(home, usageScope);
|
|
352
|
+
probe = await probeKimiStatus(home, usageScope, opts?.signal);
|
|
353
353
|
else
|
|
354
|
-
probe = await probeDroidStatus(home, usageScope);
|
|
354
|
+
probe = await probeDroidStatus(home, usageScope, opts?.signal);
|
|
355
355
|
return { verdict: verdictFromProbe(probe), checkedAt, detail: probeDetail(probe) };
|
|
356
356
|
}
|
|
357
357
|
const info = opts?.info !== undefined ? opts.info : await getAccountInfo(agent, home).catch(() => null);
|
|
@@ -426,7 +426,7 @@ export async function probeLocalFleetAuth(opts) {
|
|
|
426
426
|
// accounts, so no same-account concurrency is left to trip the throttle.
|
|
427
427
|
const perGroup = await Promise.all(groupFleetAuthInstalls(installs, (inst) => LIVE_PROBE_AGENTS.has(inst.agent)).map(async (group) => {
|
|
428
428
|
const rep = group.probe;
|
|
429
|
-
const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive });
|
|
429
|
+
const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive, signal: opts?.signal });
|
|
430
430
|
health.account = authAccountLabel(rep.info);
|
|
431
431
|
if (health.verdict === 'unconfigured')
|
|
432
432
|
return [];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** True when `AGENTS_PROFILE_BOOT` is set — callers can skip building label strings. */
|
|
2
|
+
export declare function bootProfileEnabled(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Record a named stage boundary. No-op unless `AGENTS_PROFILE_BOOT` is set, so
|
|
5
|
+
* this is free to call unconditionally on the launch path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bootMark(label: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Print the collected timeline to stderr, once. Called right before the harness
|
|
10
|
+
* child is spawned (the end of the pre-exec window) and again as an
|
|
11
|
+
* `process.on('exit')` backstop for launch paths that error out before spawn.
|
|
12
|
+
* `reason` labels the final boundary (e.g. `spawn`, `exit`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function flushBootProfile(reason: string): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time profiler for the `agents run` pre-exec phase (PHNX-3585).
|
|
3
|
+
*
|
|
4
|
+
* The AGI EXT "New Claude" boot spends the whole `agents run` wrapper cost
|
|
5
|
+
* BEFORE the harness prints anything — version resolution, account rotation,
|
|
6
|
+
* config sync, login preflight. This module makes that window measurable
|
|
7
|
+
* without a debugger: gate it on `AGENTS_PROFILE_BOOT=1` and the run path
|
|
8
|
+
* stamps named marks, then flushes a per-stage timeline to stderr the instant
|
|
9
|
+
* before the child is spawned (the moment the wrapper's work ends).
|
|
10
|
+
*
|
|
11
|
+
* All marks are `performance.now()` values, which Node measures from
|
|
12
|
+
* `performance.timeOrigin` ≈ process start — so a mark's absolute value is
|
|
13
|
+
* "ms since the `agents` process began", and consecutive marks give the cost
|
|
14
|
+
* of each stage. When the env flag is off, every function here is a couple of
|
|
15
|
+
* cheap branches and pushes nothing, so it is safe to leave wired on the hot
|
|
16
|
+
* path (the committed `scripts/bench-boot.sh` benchmark drives it).
|
|
17
|
+
*/
|
|
18
|
+
import { performance } from 'node:perf_hooks';
|
|
19
|
+
const ENABLED = process.env.AGENTS_PROFILE_BOOT === '1' || process.env.AGENTS_PROFILE_BOOT === 'true';
|
|
20
|
+
const marks = [];
|
|
21
|
+
let flushed = false;
|
|
22
|
+
/** True when `AGENTS_PROFILE_BOOT` is set — callers can skip building label strings. */
|
|
23
|
+
export function bootProfileEnabled() {
|
|
24
|
+
return ENABLED;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Record a named stage boundary. No-op unless `AGENTS_PROFILE_BOOT` is set, so
|
|
28
|
+
* this is free to call unconditionally on the launch path.
|
|
29
|
+
*/
|
|
30
|
+
export function bootMark(label) {
|
|
31
|
+
if (!ENABLED)
|
|
32
|
+
return;
|
|
33
|
+
marks.push({ label, at: performance.now() });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Print the collected timeline to stderr, once. Called right before the harness
|
|
37
|
+
* child is spawned (the end of the pre-exec window) and again as an
|
|
38
|
+
* `process.on('exit')` backstop for launch paths that error out before spawn.
|
|
39
|
+
* `reason` labels the final boundary (e.g. `spawn`, `exit`).
|
|
40
|
+
*/
|
|
41
|
+
export function flushBootProfile(reason) {
|
|
42
|
+
if (!ENABLED || flushed)
|
|
43
|
+
return;
|
|
44
|
+
flushed = true;
|
|
45
|
+
bootMark(reason);
|
|
46
|
+
if (marks.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const start = 0; // process start
|
|
49
|
+
const end = marks[marks.length - 1].at;
|
|
50
|
+
const width = Math.max(...marks.map((m) => m.label.length));
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`[boot-profile] pre-exec timeline (total ${(end - start).toFixed(1)}ms since process start)`);
|
|
53
|
+
let prev = start;
|
|
54
|
+
for (const m of marks) {
|
|
55
|
+
const delta = m.at - prev;
|
|
56
|
+
prev = m.at;
|
|
57
|
+
lines.push(` ${m.label.padEnd(width)} +${delta.toFixed(1).padStart(7)}ms @${m.at.toFixed(1).padStart(8)}ms`);
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
60
|
+
}
|
|
61
|
+
// Backstop: a run that exits before reaching the spawn (a login dead-end, a
|
|
62
|
+
// missing install) still emits whatever stages it reached, so the profile is
|
|
63
|
+
// never silently empty.
|
|
64
|
+
if (ENABLED) {
|
|
65
|
+
process.on('exit', () => flushBootProfile('exit'));
|
|
66
|
+
}
|
|
@@ -13,6 +13,18 @@ export interface CallerIdentity {
|
|
|
13
13
|
* agent comm is found.
|
|
14
14
|
*/
|
|
15
15
|
export declare function resolveCallerIdentity(env?: NodeJS.ProcessEnv): CallerIdentity;
|
|
16
|
+
/**
|
|
17
|
+
* The env a `browser --device` dispatch forwards so the HUB resolves the SAME
|
|
18
|
+
* caller identity the worker would stamp locally. Without it a forwarded verb
|
|
19
|
+
* lands on the hub with BLANK identity, and the daemon's no-identity task bucket
|
|
20
|
+
* matches any other no-identity task — so a cold verb could attach to (or a
|
|
21
|
+
* `done`/`stop` could close) an unrelated agent's or a human's browser. This is
|
|
22
|
+
* the same surface-parity fix the `run --device` path already carries via
|
|
23
|
+
* `--env AGENT_LAUNCH_ID` (remote-session-id.ts). Absent fields are omitted so a
|
|
24
|
+
* genuinely-unidentifiable caller forwards nothing rather than an empty string
|
|
25
|
+
* that would read as a real (blank) identity on the far side.
|
|
26
|
+
*/
|
|
27
|
+
export declare function callerIdentityEnv(caller: Pick<CallerIdentity, 'sessionId' | 'launchId'>): Record<string, string>;
|
|
16
28
|
/**
|
|
17
29
|
* True when a live task belongs to this caller. Match on sessionId or
|
|
18
30
|
* launchId — either is enough. A task with no recorded identity never
|
|
@@ -141,6 +141,25 @@ export function resolveCallerIdentity(env = process.env) {
|
|
|
141
141
|
}
|
|
142
142
|
return { actor, sessionId, launchId, agentPid };
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The env a `browser --device` dispatch forwards so the HUB resolves the SAME
|
|
146
|
+
* caller identity the worker would stamp locally. Without it a forwarded verb
|
|
147
|
+
* lands on the hub with BLANK identity, and the daemon's no-identity task bucket
|
|
148
|
+
* matches any other no-identity task — so a cold verb could attach to (or a
|
|
149
|
+
* `done`/`stop` could close) an unrelated agent's or a human's browser. This is
|
|
150
|
+
* the same surface-parity fix the `run --device` path already carries via
|
|
151
|
+
* `--env AGENT_LAUNCH_ID` (remote-session-id.ts). Absent fields are omitted so a
|
|
152
|
+
* genuinely-unidentifiable caller forwards nothing rather than an empty string
|
|
153
|
+
* that would read as a real (blank) identity on the far side.
|
|
154
|
+
*/
|
|
155
|
+
export function callerIdentityEnv(caller) {
|
|
156
|
+
const env = {};
|
|
157
|
+
if (caller.sessionId)
|
|
158
|
+
env.AGENT_SESSION_ID = caller.sessionId;
|
|
159
|
+
if (caller.launchId)
|
|
160
|
+
env.AGENT_LAUNCH_ID = caller.launchId;
|
|
161
|
+
return env;
|
|
162
|
+
}
|
|
144
163
|
/**
|
|
145
164
|
* True when a live task belongs to this caller. Match on sessionId or
|
|
146
165
|
* launchId — either is enough. A task with no recorded identity never
|
|
@@ -4,24 +4,24 @@ import type { IPCRequest, IPCResponse } from './types.js';
|
|
|
4
4
|
export interface IPCRequestOptions {
|
|
5
5
|
autoStartDaemon?: boolean;
|
|
6
6
|
}
|
|
7
|
-
export declare class
|
|
7
|
+
export declare class BrowserServiceNotRunningError extends Error {
|
|
8
8
|
constructor();
|
|
9
9
|
}
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function formatBrowserServiceNotRunningError(daemonRunning?: boolean): string;
|
|
11
11
|
export declare function getSocketPath(): string;
|
|
12
12
|
/** Is the daemon reachable? A real connect probe on every platform — a socket
|
|
13
13
|
* file existing on disk is not proof a daemon is listening on it. */
|
|
14
|
-
export declare function
|
|
14
|
+
export declare function isBrowserServiceReachable(): Promise<boolean>;
|
|
15
15
|
/**
|
|
16
16
|
* Is a *reachable* daemon actually responsive, or is its event loop wedged
|
|
17
|
-
* (PHNX-3411)? Retries {@link
|
|
17
|
+
* (PHNX-3411)? Retries {@link probeBrowserServiceResponsive} up to
|
|
18
18
|
* {@link RESPONSIVENESS_PROBE_ATTEMPTS} times so a single transient miss (a GC
|
|
19
19
|
* pause) never condemns a healthy daemon; returns true as soon as any attempt
|
|
20
20
|
* gets a reply, and false only when every attempt fails.
|
|
21
21
|
*/
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function isBrowserServiceResponsive(endpoint?: string, attempts?: number): Promise<boolean>;
|
|
23
23
|
/**
|
|
24
|
-
* Wait until the browser
|
|
24
|
+
* Wait until the browser service is genuinely reachable, or throw.
|
|
25
25
|
*
|
|
26
26
|
* Re-probes across the whole window rather than latching on the first accept, so
|
|
27
27
|
* it survives an IPC-server restart that happens mid-wait (PHNX-3289): a restart
|
|
@@ -30,32 +30,35 @@ export declare function isDaemonResponsive(endpoint?: string, attempts?: number)
|
|
|
30
30
|
* that never comes up throws a message naming the endpoint and the budget, never
|
|
31
31
|
* a silent hang.
|
|
32
32
|
*/
|
|
33
|
-
export declare function
|
|
34
|
-
/** Outcome of {@link
|
|
35
|
-
export interface
|
|
36
|
-
/** The
|
|
33
|
+
export declare function waitForBrowserService(_socketPath: string, timeoutMs?: number): Promise<void>;
|
|
34
|
+
/** Outcome of {@link stopBrowserService} — what the service-scoped stop did. */
|
|
35
|
+
export interface BrowserServiceStopResult {
|
|
36
|
+
/** The browser IPC service was reachable before the stop was requested. */
|
|
37
37
|
wasRunning: boolean;
|
|
38
|
+
/** The shared daemon stayed running while only browser IPC was stopped. */
|
|
39
|
+
daemonRunning: boolean;
|
|
38
40
|
/** A leftover `browser.sock` file was unlinked (POSIX only). */
|
|
39
41
|
socketCleared: boolean;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
43
|
-
* (PHNX-
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* Stop only the daemon-hosted browser IPC service so a subsequent `start` comes
|
|
45
|
+
* up clean (PHNX-3605). The short-lived browser client persists the
|
|
46
|
+
* `browser-ipc` service off and asks the daemon to apply that transition over
|
|
47
|
+
* its existing SIGHUP control path. It never stops or restarts the shared
|
|
48
|
+
* supervisor, so routines, usage sync, secrets, and every sibling service keep
|
|
49
|
+
* their process and in-flight work.
|
|
48
50
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* Once the endpoint is quiet, a stale socket left by a hard-crashed browser
|
|
52
|
+
* service is removed. If the daemon event loop itself is wedged and cannot
|
|
53
|
+
* consume SIGHUP, the endpoint remains reachable and this fails loud with the
|
|
54
|
+
* deliberate operator-owned whole-daemon restart command; the client never
|
|
55
|
+
* escalates into that process-wide action itself.
|
|
53
56
|
*/
|
|
54
|
-
export declare function
|
|
57
|
+
export declare function stopBrowserService(): Promise<BrowserServiceStopResult>;
|
|
55
58
|
/**
|
|
56
59
|
* Remove a leftover browser socket FILE, but only when nothing is listening on
|
|
57
60
|
* it — re-probing liveness IMMEDIATELY before the unlink to close the TOCTOU
|
|
58
|
-
* window (PHNX-3289 review). Between {@link
|
|
61
|
+
* window (PHNX-3289 review). Between {@link stopBrowserService}'s quiesce loop
|
|
59
62
|
* deciding the endpoint was unreachable and this unlink, a concurrent
|
|
60
63
|
* `browser start` could bind a NEW listener on the same path; an unconditional
|
|
61
64
|
* unlink would then delete a LIVE daemon's socket — the exact two-servers orphan
|
|
@@ -65,7 +68,7 @@ export declare function resetBrowserDaemon(): Promise<BrowserDaemonResetResult>;
|
|
|
65
68
|
*/
|
|
66
69
|
export declare function clearDeadSocketFile(endpoint: string, socketPath: string): Promise<boolean>;
|
|
67
70
|
/**
|
|
68
|
-
* One long-lived connection to the existing browser
|
|
71
|
+
* One long-lived connection to the existing browser service. Requests are
|
|
69
72
|
* serialized so the daemon's newline-delimited responses always map to the
|
|
70
73
|
* caller that produced them, while the process and socket stay warm between
|
|
71
74
|
* actions.
|
|
@@ -85,6 +88,8 @@ export declare class BrowserIPCConnection {
|
|
|
85
88
|
}
|
|
86
89
|
export declare class BrowserIPCServer {
|
|
87
90
|
private server;
|
|
91
|
+
/** Filesystem identity of the POSIX binding this server created. */
|
|
92
|
+
private socketIdentity;
|
|
88
93
|
private service;
|
|
89
94
|
/** Live client connections, so {@link stop} can end them rather than wait. */
|
|
90
95
|
private connections;
|
|
@@ -128,21 +133,21 @@ export declare class BrowserIPCServer {
|
|
|
128
133
|
private handleRequest;
|
|
129
134
|
}
|
|
130
135
|
/**
|
|
131
|
-
* Decide whether a running daemon is stale
|
|
136
|
+
* Decide whether a running daemon is stale enough to name an operator refresh.
|
|
132
137
|
*
|
|
133
|
-
* FORWARD ONLY:
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* not flap the daemon
|
|
138
|
+
* FORWARD ONLY: recommend a refresh when the daemon is *older* than this CLI.
|
|
139
|
+
* An older CLI rides a newer daemon without noise. A client never performs the
|
|
140
|
+
* refresh itself — two installs sharing one daemon dir (keyed off $HOME) must
|
|
141
|
+
* not flap the daemon or interrupt one another's hosted services.
|
|
137
142
|
*
|
|
138
143
|
* `undefined`/`'unknown'` means the daemon is too old to answer the `version`
|
|
139
|
-
* action reliably — don't
|
|
144
|
+
* action reliably — don't recommend an action on that ambiguous signal.
|
|
140
145
|
*
|
|
141
146
|
* When numeric compare cannot order two distinct strings (e.g. two
|
|
142
|
-
* `0.0.0-dev.*` builds),
|
|
143
|
-
*
|
|
147
|
+
* `0.0.0-dev.*` builds), surface the mismatch without deciding either client
|
|
148
|
+
* owns the shared process.
|
|
144
149
|
*/
|
|
145
|
-
export declare function
|
|
150
|
+
export declare function shouldRecommendDaemonRefresh(daemonVersion: string | undefined, clientVersion: string): boolean;
|
|
146
151
|
export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOptions): Promise<IPCResponse>;
|
|
147
152
|
/**
|
|
148
153
|
* Fill actor / launchId / sessionId from the calling process when the request
|