@phnx-labs/agents-cli 1.22.6 → 1.22.8

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +131 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +61 -0
  5. package/dist/commands/exec.js +83 -16
  6. package/dist/commands/feed.d.ts +2 -1
  7. package/dist/commands/feed.js +47 -20
  8. package/dist/commands/focus.js +22 -1
  9. package/dist/commands/harness.d.ts +0 -1
  10. package/dist/commands/harness.js +60 -4
  11. package/dist/commands/models.js +2 -2
  12. package/dist/commands/monitors.js +2 -2
  13. package/dist/commands/projects.js +122 -107
  14. package/dist/commands/routines.js +2 -2
  15. package/dist/commands/run-account-picker.js +2 -0
  16. package/dist/commands/secrets.js +1 -1
  17. package/dist/commands/sessions-backfill.d.ts +33 -0
  18. package/dist/commands/sessions-backfill.js +83 -1
  19. package/dist/commands/sessions-stats.d.ts +36 -0
  20. package/dist/commands/sessions-stats.js +263 -0
  21. package/dist/commands/sessions.d.ts +1 -1
  22. package/dist/commands/sessions.js +21 -1
  23. package/dist/commands/snapshot.d.ts +11 -0
  24. package/dist/commands/snapshot.js +107 -0
  25. package/dist/commands/teams.js +2 -1
  26. package/dist/commands/view.d.ts +9 -1
  27. package/dist/commands/view.js +63 -12
  28. package/dist/index.js +4 -2
  29. package/dist/lib/activity.js +2 -2
  30. package/dist/lib/agents.js +68 -11
  31. package/dist/lib/analytics/recipes.js +11 -5
  32. package/dist/lib/browser/ipc.js +2 -0
  33. package/dist/lib/browser/profiles.d.ts +15 -7
  34. package/dist/lib/browser/profiles.js +53 -12
  35. package/dist/lib/browser/remote-control.d.ts +35 -0
  36. package/dist/lib/browser/remote-control.js +48 -0
  37. package/dist/lib/browser/service.d.ts +19 -0
  38. package/dist/lib/browser/service.js +19 -1
  39. package/dist/lib/browser/types.d.ts +14 -2
  40. package/dist/lib/byok-usage.d.ts +38 -0
  41. package/dist/lib/byok-usage.js +117 -0
  42. package/dist/lib/capabilities.js +1 -1
  43. package/dist/lib/device-config.js +8 -0
  44. package/dist/lib/exec.d.ts +20 -0
  45. package/dist/lib/exec.js +73 -8
  46. package/dist/lib/feed-outcome.d.ts +1 -0
  47. package/dist/lib/feed-outcome.js +2 -0
  48. package/dist/lib/feed-post.js +1 -1
  49. package/dist/lib/feed-ranking.d.ts +1 -0
  50. package/dist/lib/feed-ranking.js +4 -0
  51. package/dist/lib/feed.d.ts +4 -1
  52. package/dist/lib/feed.js +25 -0
  53. package/dist/lib/hosts/passthrough.d.ts +10 -1
  54. package/dist/lib/hosts/passthrough.js +23 -3
  55. package/dist/lib/hosts/remote-cmd.js +1 -0
  56. package/dist/lib/mcp.js +6 -1
  57. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  58. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  59. package/dist/lib/model-tiers.js +4 -1
  60. package/dist/lib/models.js +63 -0
  61. package/dist/lib/placement.d.ts +82 -0
  62. package/dist/lib/placement.js +188 -0
  63. package/dist/lib/profiles.d.ts +73 -10
  64. package/dist/lib/profiles.js +133 -15
  65. package/dist/lib/project-focus.d.ts +9 -0
  66. package/dist/lib/project-focus.js +23 -0
  67. package/dist/lib/project-key.d.ts +1 -1
  68. package/dist/lib/project-key.js +1 -1
  69. package/dist/lib/project-probe.d.ts +18 -0
  70. package/dist/lib/project-probe.js +46 -0
  71. package/dist/lib/project-status.d.ts +56 -0
  72. package/dist/lib/project-status.js +125 -18
  73. package/dist/lib/resources/mcp.js +3 -0
  74. package/dist/lib/resources/types.d.ts +1 -1
  75. package/dist/lib/rotate.d.ts +19 -4
  76. package/dist/lib/rotate.js +24 -1
  77. package/dist/lib/routines.d.ts +2 -0
  78. package/dist/lib/runner.d.ts +3 -0
  79. package/dist/lib/runner.js +90 -7
  80. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  82. package/dist/lib/secrets/audit.js +1 -1
  83. package/dist/lib/secrets/index.d.ts +1 -0
  84. package/dist/lib/secrets/index.js +29 -15
  85. package/dist/lib/secrets/remote.d.ts +1 -1
  86. package/dist/lib/secrets/remote.js +1 -1
  87. package/dist/lib/session/active.d.ts +2 -0
  88. package/dist/lib/session/bash-command.d.ts +2 -3
  89. package/dist/lib/session/db.d.ts +92 -1
  90. package/dist/lib/session/db.js +230 -1
  91. package/dist/lib/session/digest.d.ts +1 -1
  92. package/dist/lib/session/digest.js +1 -1
  93. package/dist/lib/share/publish.js +24 -0
  94. package/dist/lib/snapshot.d.ts +103 -0
  95. package/dist/lib/snapshot.js +99 -0
  96. package/dist/lib/startup/command-registry.d.ts +1 -1
  97. package/dist/lib/startup/command-registry.js +2 -2
  98. package/dist/lib/subagents-registry.js +3 -0
  99. package/dist/lib/types.d.ts +11 -2
  100. package/dist/lib/usage.d.ts +5 -0
  101. package/dist/lib/usage.js +3 -3
  102. package/package.json +1 -1
  103. package/dist/commands/activity.d.ts +0 -87
  104. package/dist/commands/activity.js +0 -346
@@ -13,6 +13,8 @@ import { getProjectRunConfigs } from './run-config.js';
13
13
  import { emit } from './events.js';
14
14
  import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
15
15
  import { readAccountHeadroom } from './fleet-cache.js';
16
+ import { machineId } from './machine-id.js';
17
+ import { readAuthHealthCache, authCacheKey, isDeadVerdict } from './auth-health.js';
16
18
  function getRotateDir() {
17
19
  const dir = path.join(getHelpersDir(), 'rotate');
18
20
  fs.mkdirSync(dir, { recursive: true });
@@ -65,8 +67,14 @@ export function setGlobalRunStrategy(agent, strategy) {
65
67
  meta.run[agent] = { ...(meta.run[agent] ?? {}), strategy };
66
68
  writeMeta(meta);
67
69
  }
70
+ /**
71
+ * Whether an account may be rotated INTO right now. Defined in terms of
72
+ * {@link readinessFromCandidate} so the router's pick gate and the pre-flight
73
+ * warning can never disagree: an account is eligible iff its readiness is
74
+ * `ready` — signed in, not server-revoked, and not out of usage.
75
+ */
68
76
  function isRotationEligible(candidate) {
69
- return candidate.signedIn && hasUsageAvailable(candidate);
77
+ return readinessFromCandidate(candidate).ready;
70
78
  }
71
79
  /**
72
80
  * Whether a version home can actually authenticate a launch.
@@ -146,6 +154,13 @@ export function readinessFromCandidate(candidate) {
146
154
  if (!candidate.signedIn) {
147
155
  return { ready: false, reason: 'signed_out', email: candidate.email };
148
156
  }
157
+ // A token the daemon's live probe saw rejected (401/403 -> `revoked`) will fail
158
+ // auth at spawn no matter how much usage headroom it has. Exclude it BEFORE the
159
+ // usage gate so rotation never routes into a doomed login. Fail-open: any other
160
+ // (or null) verdict does not gate — see `RotateCandidate.authVerdict`.
161
+ if (candidate.authVerdict !== null && isDeadVerdict(candidate.authVerdict)) {
162
+ return { ready: false, reason: 'revoked', email: candidate.email };
163
+ }
149
164
  if (hasUsageAvailable(candidate)) {
150
165
  return { ready: true };
151
166
  }
@@ -495,6 +510,12 @@ export function formatNoHealthyHarnessError(summaries, nowMs = Date.now()) {
495
510
  }
496
511
  export async function collectRunCandidates(agent) {
497
512
  const versions = listInstalledVersions(agent);
513
+ // Read the local auth-health probe cache once (cache-only, no network — the
514
+ // daemon is the sole writer). A `revoked` verdict for a (host, agent, version)
515
+ // excludes that account from the pick; a missing row is fail-open. Keyed by the
516
+ // LOCAL host — routing decides which local version to launch.
517
+ const authCache = readAuthHealthCache();
518
+ const localHost = machineId();
498
519
  const rows = await Promise.all(versions.map(async (version) => {
499
520
  const home = getVersionHomePath(agent, version);
500
521
  const info = await getAccountInfo(agent, home);
@@ -510,6 +531,7 @@ export async function collectRunCandidates(agent) {
510
531
  // lives — see isLaunchableSignedIn. Do not reuse the active-home fallback
511
532
  // identity for routing, or empty version homes look healthy and die at spawn.
512
533
  const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
534
+ const authVerdict = authCache[authCacheKey(localHost, agent, version)]?.verdict ?? null;
513
535
  return {
514
536
  agent,
515
537
  version,
@@ -521,6 +543,7 @@ export async function collectRunCandidates(agent) {
521
543
  usageStatus: launchable ? info.usageStatus : null,
522
544
  plan: launchable ? info.plan : null,
523
545
  signedIn: launchable,
546
+ authVerdict,
524
547
  lastActive: info.lastActive,
525
548
  };
526
549
  }));
@@ -239,6 +239,8 @@ export interface RunMeta {
239
239
  pid: number | null;
240
240
  /** Process birth time (epoch ms) recorded at spawn for pid-reuse detection. */
241
241
  spawnedAt?: number;
242
+ /** Configured execution deadline persisted for daemon-restart recovery. */
243
+ timeoutMs?: number;
242
244
  /**
243
245
  * `missed` is not an execution outcome — it is the record that a scheduled
244
246
  * fire never happened (the daemon was down, asleep, or wedged when it came
@@ -23,6 +23,9 @@ export interface RunResult {
23
23
  meta: RunMeta;
24
24
  reportPath: string | null;
25
25
  }
26
+ export declare class RoutineAlreadyRunningError extends Error {
27
+ constructor(jobName: string, runId: string);
28
+ }
26
29
  /** Agents the daemon can actually run, derived from the command table above
27
30
  * so the `--agent` help and any validation can never drift from it. */
28
31
  export declare const ROUTINE_AGENT_IDS: readonly string[];
@@ -17,7 +17,7 @@ import { spawn, execFileSync } from 'child_process';
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
19
19
  import * as os from 'os';
20
- import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
20
+ import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
21
21
  import { getRunsDir } from './state.js';
22
22
  import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
23
23
  import { resolveModel, buildReasoningFlags } from './models.js';
@@ -26,7 +26,9 @@ import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, dete
26
26
  import { resolveActor } from './actor.js';
27
27
  import { loadTask as loadHostTask } from './hosts/tasks.js';
28
28
  import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
29
- import { backgroundSpawnOptions } from './platform/process.js';
29
+ import { backgroundSpawnOptions, killTree } from './platform/process.js';
30
+ import lockfile from 'proper-lockfile';
31
+ import { ensureLockTarget } from './fs-atomic.js';
30
32
  import { walkForFiles } from './fs-walk.js';
31
33
  import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js';
32
34
  import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from './rotate.js';
@@ -34,6 +36,66 @@ import { readAuthHealth, isDeadVerdict } from './auth-health.js';
34
36
  import { machineId } from './machine-id.js';
35
37
  import { isSelfUpdatingAgent } from './agents.js';
36
38
  import { expandLocalHome, getProjectRoot } from './project-root.js';
39
+ export class RoutineAlreadyRunningError extends Error {
40
+ constructor(jobName, runId) {
41
+ super(`Routine '${jobName}' already has a running execution (${runId})`);
42
+ this.name = 'RoutineAlreadyRunningError';
43
+ }
44
+ }
45
+ const ROUTINE_LAUNCH_LOCK_STALE_MS = 30_000;
46
+ const ROUTINE_LAUNCH_LOCK_WAIT_MS = 10_000;
47
+ function activeRoutineRun(config) {
48
+ const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
49
+ const now = Date.now();
50
+ const runs = listRuns(config.name);
51
+ for (let i = runs.length - 1; i >= 0; i--) {
52
+ const run = runs[i];
53
+ if (run.status !== 'running')
54
+ continue;
55
+ if (run.pid && isPidOurs(run.pid, run.spawnedAt))
56
+ return run;
57
+ if (!run.pid) {
58
+ const startedAt = Date.parse(run.startedAt);
59
+ if (Number.isFinite(startedAt) && now - startedAt < (run.timeoutMs ?? timeoutMs))
60
+ return run;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ async function withRoutineLaunchClaim(config, launch) {
66
+ const target = path.join(getJobRunsDir(config.name), '.launch-claim');
67
+ ensureLockTarget(target, '', 0o700);
68
+ const release = await lockfile.lock(target, {
69
+ stale: ROUTINE_LAUNCH_LOCK_STALE_MS,
70
+ retries: {
71
+ retries: Math.ceil(ROUTINE_LAUNCH_LOCK_WAIT_MS / 100),
72
+ factor: 1,
73
+ minTimeout: 100,
74
+ maxTimeout: 100,
75
+ },
76
+ });
77
+ try {
78
+ const active = activeRoutineRun(config);
79
+ if (active)
80
+ throw new RoutineAlreadyRunningError(config.name, active.runId);
81
+ return await launch();
82
+ }
83
+ finally {
84
+ await release();
85
+ }
86
+ }
87
+ function terminateRoutineTree(pid) {
88
+ if (!pid)
89
+ return;
90
+ if (process.platform === 'win32') {
91
+ killTree(pid);
92
+ return;
93
+ }
94
+ try {
95
+ process.kill(-pid, 'SIGKILL');
96
+ }
97
+ catch { /* already exited */ }
98
+ }
37
99
  /** CLI command templates per agent, with {prompt} as a placeholder. */
38
100
  const AGENT_COMMANDS = {
39
101
  claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
@@ -1086,6 +1148,9 @@ export async function executeJobDetached(config, hooks) {
1086
1148
  process.stderr.write(`[agents] daemon: skipping '${config.name}' — ${eligibility.message}\n`);
1087
1149
  throw new Error(eligibility.message);
1088
1150
  }
1151
+ return withRoutineLaunchClaim(config, () => executeJobDetachedClaimed(config, hooks));
1152
+ }
1153
+ async function executeJobDetachedClaimed(config, hooks) {
1089
1154
  // Placement (hostStrategy / bare host:) — dispatch off-box and return; the
1090
1155
  // monitor finalizes host: runs, cloud runs stay terminal when dispatch ends.
1091
1156
  // Either way the in-process onFinish hook does not fire for off-box routines
@@ -1161,6 +1226,7 @@ export async function executeJobDetached(config, hooks) {
1161
1226
  ...(config.workflow ? { workflow: config.workflow } : {}),
1162
1227
  pid: null,
1163
1228
  spawnedAt: Date.now(),
1229
+ timeoutMs: parseTimeout(config.timeout) || 10 * 60 * 1000,
1164
1230
  status: 'running',
1165
1231
  startedAt: new Date().toISOString(),
1166
1232
  completedAt: null,
@@ -1195,10 +1261,13 @@ export async function executeJobDetached(config, hooks) {
1195
1261
  env: spawnEnv,
1196
1262
  });
1197
1263
  let settled = false;
1264
+ let timeoutTimer;
1198
1265
  const settle = (status, exitCode, errorMessage) => {
1199
1266
  if (settled)
1200
1267
  return;
1201
1268
  settled = true;
1269
+ if (timeoutTimer)
1270
+ clearTimeout(timeoutTimer);
1202
1271
  finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
1203
1272
  writeRunMeta(meta);
1204
1273
  archiveRoutineTranscripts(meta, runDir, overlayHome);
@@ -1212,6 +1281,10 @@ export async function executeJobDetached(config, hooks) {
1212
1281
  // can read report.md (RUSH-2030). Best-effort; never breaks finalization.
1213
1282
  safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
1214
1283
  };
1284
+ timeoutTimer = setTimeout(() => {
1285
+ terminateRoutineTree(child.pid ?? null);
1286
+ settle('timeout', null, 'exceeded configured timeout');
1287
+ }, meta.timeoutMs);
1215
1288
  child.on('exit', (code) => {
1216
1289
  let logText = '';
1217
1290
  try {
@@ -1249,7 +1322,7 @@ export async function executeJobDetached(config, hooks) {
1249
1322
  catch { /* fd already closed */ }
1250
1323
  meta.pid = child.pid || null;
1251
1324
  writeRunMeta(meta);
1252
- return meta;
1325
+ return { ...meta };
1253
1326
  }
1254
1327
  /**
1255
1328
  * Detached (fire-and-forget) execution for a command-mode routine. Mirrors the
@@ -1288,6 +1361,7 @@ function executeCommandJobDetached(config, hooks) {
1288
1361
  command: config.command,
1289
1362
  pid: null,
1290
1363
  spawnedAt: Date.now(),
1364
+ timeoutMs: parseTimeout(config.timeout) || 10 * 60 * 1000,
1291
1365
  status: 'running',
1292
1366
  startedAt: new Date().toISOString(),
1293
1367
  completedAt: null,
@@ -1302,17 +1376,24 @@ function executeCommandJobDetached(config, hooks) {
1302
1376
  // fire-and-forget call, so the exit event fires here. (monitorRunningJobs no
1303
1377
  // longer force-fails command jobs; it reads exit-code only on the restart edge.)
1304
1378
  let settled = false;
1379
+ let timeoutTimer;
1305
1380
  const settle = (status, exitCode, errorMessage) => {
1306
1381
  if (settled)
1307
1382
  return;
1308
1383
  settled = true;
1384
+ if (timeoutTimer)
1385
+ clearTimeout(timeoutTimer);
1309
1386
  finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
1310
1387
  writeRunMeta(meta);
1311
- timer.end({ status, exitCode, runId, ...(errorMessage ? { error: errorMessage } : {}) });
1388
+ timer.end({ status, exitCode: exitCode ?? undefined, runId, ...(errorMessage ? { error: errorMessage } : {}) });
1312
1389
  // Finish notification (RUSH-2030). For command routines the threshold only
1313
1390
  // surfaces failures, decided in routine-notify.ts. Best-effort.
1314
1391
  safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
1315
1392
  };
1393
+ timeoutTimer = setTimeout(() => {
1394
+ terminateRoutineTree(child.pid ?? null);
1395
+ settle('timeout', null, 'exceeded configured timeout');
1396
+ }, meta.timeoutMs);
1316
1397
  child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
1317
1398
  child.on('error', (err) => {
1318
1399
  settle('failed', 1, err.message);
@@ -1325,7 +1406,7 @@ function executeCommandJobDetached(config, hooks) {
1325
1406
  catch { /* fd already closed */ }
1326
1407
  meta.pid = child.pid || null;
1327
1408
  writeRunMeta(meta);
1328
- return meta;
1409
+ return { ...meta };
1329
1410
  }
1330
1411
  function extractAndSaveReport(stdoutPath, agentType, runDir) {
1331
1412
  try {
@@ -1508,8 +1589,10 @@ export function monitorRunningJobs() {
1508
1589
  // parse or extract. Reap them on pid liveness alone.
1509
1590
  const isCommandRun = Boolean(meta.command) || !meta.agent;
1510
1591
  const wallClockMs = Date.now() - Date.parse(meta.startedAt);
1511
- if (Number.isFinite(wallClockMs) && wallClockMs > MAX_WALL_CLOCK_MS) {
1512
- finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'exceeded max wall clock' });
1592
+ const timeoutMs = meta.timeoutMs ?? MAX_WALL_CLOCK_MS;
1593
+ if (Number.isFinite(wallClockMs) && wallClockMs > timeoutMs) {
1594
+ terminateRoutineTree(meta.pid);
1595
+ finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'exceeded configured timeout' });
1513
1596
  writeRunMeta(meta);
1514
1597
  if (!isCommandRun) {
1515
1598
  extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * The event vocabulary, all audit-level and non-milestone (so they surface in
21
21
  * `agents events` and the persisted audit trail, but are NOT required in the
22
- * curated `agents activity` / `agents feed` surfaces):
22
+ * curated `agents feed` surface):
23
23
  * - `secrets.get` — a value was READ (exec inject, export, `view --reveal`,
24
24
  * raw `get <item>`, remote resolve, sync push,
25
25
  * `run --secrets`, and every other bundle read).
@@ -121,6 +121,7 @@ export declare function withRawKeychainServiceNames<T>(fn: () => T): T;
121
121
  * the re-key migration and tests; runtime callers go through the primitives,
122
122
  * which apply this transparently. */
123
123
  export declare function hashedServiceName(item: string, key: Buffer): string;
124
+ export declare function readHmacKeyRecord(): HmacKeyRecord | null;
124
125
  /**
125
126
  * Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
126
127
  * migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
@@ -235,7 +235,7 @@ function parseHmacKeyRecord(raw) {
235
235
  }
236
236
  return null;
237
237
  }
238
- function readHmacKeyRecord() {
238
+ export function readHmacKeyRecord() {
239
239
  // HMAC_KEY_ITEM is exempt from the transform, so this routes to the helper
240
240
  // (or the test backend) under its literal name. The item is no-ACL, so the
241
241
  // read is silent — attest that to the storm guard so a headless hashed-name
@@ -247,7 +247,30 @@ function readHmacKeyRecord() {
247
247
  catch {
248
248
  return null;
249
249
  }
250
- return parseHmacKeyRecord(raw);
250
+ const record = parseHmacKeyRecord(raw);
251
+ // Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
252
+ // (pre the metadata/hmackey no-ACL migration fix) re-stamped this
253
+ // contractually-no-ACL item with a biometry ACL, so the read just above pops the
254
+ // generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
255
+ // `agents devices list` stats probe the SessionStart hook runs, and every other
256
+ // background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
257
+ // cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
258
+ // (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
259
+ // it never converged exactly these reads and the machine prompted forever.
260
+ // Re-store the record no-ACL once per machine here (the read that produced it has
261
+ // already happened — and already prompted if the item was ACL'd); every
262
+ // subsequent read, in this process and all future ones, is silent.
263
+ if (record && !record.healedNoAcl) {
264
+ try {
265
+ healHmacKeyNoAclOnce(record);
266
+ record.healedNoAcl = true;
267
+ }
268
+ catch {
269
+ // A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
270
+ // read) rather than a silent wrong state; the next process retries.
271
+ }
272
+ }
273
+ return record;
251
274
  }
252
275
  function writeHmacKeyRecord(rec) {
253
276
  // JSON.stringify drops undefined fields (used to clear pendingDeletes).
@@ -423,19 +446,10 @@ function maybeAutoRekey() {
423
446
  return;
424
447
  const st = resolveHashState();
425
448
  if (st.active) {
426
- // Heal an already-active machine whose hmackey was re-stamped ACL'd by an old
427
- // helper (its read popped the generic Touch ID sheet on every hashed lookup).
428
- // Runs once per machine; mutate the local so a later finishPendingDeletes write
429
- // preserves the healed flag.
430
- if (st.record && !st.record.healedNoAcl) {
431
- try {
432
- healHmacKeyNoAclOnce(st.record);
433
- st.record.healedNoAcl = true;
434
- }
435
- catch {
436
- /* next process retries */
437
- }
438
- }
449
+ // The stale-ACL'd-hmackey heal now runs on the hot read path
450
+ // (readHmacKeyRecord), which the resolveHashState() above just went through —
451
+ // so st.record is already healed here regardless of how this machine reached
452
+ // "hashing active". Nothing to do but finish any pending deletes.
439
453
  if (st.record?.pendingDeletes?.length) {
440
454
  try {
441
455
  finishPendingDeletes(st.record);
@@ -46,7 +46,7 @@ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<strin
46
46
  * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
47
47
  * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
48
48
  * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
49
- * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
49
+ * fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
50
50
  * works on the secrets remote commands too. Empty when none is set.
51
51
  */
52
52
  export declare function parseHostsOption(opts: {
@@ -77,7 +77,7 @@ export async function resolveHostSshTarget(nameOrAlias) {
77
77
  * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
78
78
  * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
79
79
  * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
80
- * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
80
+ * fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
81
81
  * works on the secrets remote commands too. Empty when none is set.
82
82
  */
83
83
  export function parseHostsOption(opts) {
@@ -58,6 +58,8 @@ export interface ActiveSession {
58
58
  pid?: number;
59
59
  sessionId?: string;
60
60
  cwd?: string;
61
+ /** Project/repo key derived from cwd, when known. */
62
+ project?: string | null;
61
63
  /** User-given name from /rename command. */
62
64
  label?: string;
63
65
  /** Durable `agents run --name` launch handle, when the run was named. */
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Parses the raw command strings agents pass to Bash tool calls into structured
3
3
  * metadata: the executable, category, subcommand, and a display summary. Used by
4
- * session rendering and the activity-log hook so `agents sessions` and
5
- * `agents activity` can summarize what actually happened instead of printing a
6
- * wall of shell.
4
+ * session rendering and the activity-log hook so `agents sessions` can
5
+ * summarize what actually happened instead of printing a wall of shell.
7
6
  */
8
7
  export type BashCategory = 'vcs' | 'build-test' | 'install' | 'remote' | 'http' | 'media' | 'upscaling' | 'metadata' | 'probe' | 'search' | 'shell' | 'wait' | 'other';
9
8
  export interface BashToolInfo {
@@ -12,7 +12,14 @@ import { type IndexedToolCall } from './tool-calls.js';
12
12
  /** Current schema version; bumped when migrations are added. Exported so tests
13
13
  * assert against the constant instead of hardcoding a number that every bump
14
14
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
15
- export declare const SCHEMA_VERSION = 30;
15
+ export declare const SCHEMA_VERSION = 31;
16
+ /**
17
+ * Bump to force `agents sessions backfill resources` to re-derive every
18
+ * session's skill/slash-command tallies on its next run (resource_scan_ledger
19
+ * rows with a lower version are treated as stale — the same mechanism
20
+ * TOOL_INDEX_VERSION gives the tool backfill).
21
+ */
22
+ export declare const RESOURCE_INDEX_VERSION = 1;
16
23
  /** Raw row shape returned from the sessions table. */
17
24
  export interface SessionRow {
18
25
  id: string;
@@ -75,6 +82,8 @@ export interface QueryOptions {
75
82
  /** Match any session whose cwd equals this or is a descendant of it. */
76
83
  cwdPrefix?: string;
77
84
  project?: string;
85
+ /** Only sessions recorded on this machine (host), case-insensitive. */
86
+ machine?: string;
78
87
  /** Match the full session id or short id, case-insensitively (exact). */
79
88
  idExact?: string;
80
89
  /** Match sessions whose id or short id begins with this (case-insensitive prefix). */
@@ -324,6 +333,88 @@ export declare function queryAffinityRollup(options: {
324
333
  export declare function queryUsageRollup(options: QueryOptions & {
325
334
  groupBy: UsageRollupGroup;
326
335
  }): UsageRollupRow[];
336
+ /** One aggregated resource (skill or slash-command) in a usage-stats rollup. */
337
+ export interface ResourceStatRow {
338
+ /** 'skill' or 'command' (singular, as stored in session_resource_usage.kind). */
339
+ kind: string;
340
+ /** Stored resource name — bare, or `plugin:short` for a plugin-owned resource. */
341
+ name: string;
342
+ /** Owning plugin, or null for a flat (non-namespaced) resource. */
343
+ plugin: string | null;
344
+ /** DotAgents layer or plugin marketplace the resource resolved to at write time. */
345
+ source: string | null;
346
+ /** Distinct sessions that invoked this resource within the filter window. */
347
+ sessions: number;
348
+ /** Total invocations (sum of per-session counts) within the window. */
349
+ invocations: number;
350
+ }
351
+ /**
352
+ * Roll up skill / slash-command usage from session_resource_usage, joined to
353
+ * `sessions` for attribution so the same filter shape as querySessions
354
+ * (agent / project / since / machine) narrows WHICH sessions count. Grouped by
355
+ * resource identity (kind + name + plugin + source) and ordered by invocation
356
+ * volume — the read side of "which skills/commands do I actually use, and which
357
+ * are dead weight". `order: 'bottom'` ranks least-used first (the one-time
358
+ * skills); `limit` caps the returned rows.
359
+ *
360
+ * The signal only captures EXPLICIT invocations (slash commands and `Skill`
361
+ * tool calls). An auto-triggered skill (loaded by description match) emits no
362
+ * event, so it reads as zero here — a 0 means "never explicitly invoked", not
363
+ * "never loaded". Skill invocations are recorded for Claude and Kimi (the
364
+ * `Skill`-tool harnesses); slash-commands are Claude-only.
365
+ *
366
+ * `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
367
+ * distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
368
+ * — deliberately not routed through buildSessionWhere so `--plugin rush` shows
369
+ * only rush's resources rather than every resource used by a rush-touching
370
+ * session.
371
+ */
372
+ export declare function queryResourceUsageStats(options: QueryOptions & {
373
+ kind?: 'skill' | 'command';
374
+ pluginFilter?: string;
375
+ order?: 'top' | 'bottom';
376
+ limit?: number;
377
+ }): ResourceStatRow[];
378
+ /**
379
+ * Coverage of the resource-usage signal: how many distinct sessions carry any
380
+ * row in session_resource_usage vs. the total indexed. A low ratio means the
381
+ * historical backfill (`agents sessions backfill resources`) hasn't run — the
382
+ * stats surface uses this to tell the user their zero-counts may just be
383
+ * un-scanned history, not genuine non-use.
384
+ */
385
+ export declare function resourceUsageCoverage(): {
386
+ covered: number;
387
+ total: number;
388
+ };
389
+ /** Outcome of a resource-usage backfill run. */
390
+ export interface ResourceBackfillResult {
391
+ /** Sessions considered (matched the filter, had a real transcript). */
392
+ scanned: number;
393
+ /** Sessions (re)parsed and written this run. */
394
+ updated: number;
395
+ /** Sessions already current at this extractor version, skipped. */
396
+ skipped: number;
397
+ /** Sessions whose transcript could not be stat'd or parsed. */
398
+ failed: number;
399
+ /** Total session_resource_usage rows written across updated sessions. */
400
+ resourceRows: number;
401
+ }
402
+ /**
403
+ * One-shot historical backfill of session_resource_usage (#12). The normal
404
+ * incremental scan writes resource usage only for sessions whose transcript it
405
+ * (re)parses; a session indexed before this feature shipped keeps a fresh
406
+ * scan_ledger row and is never re-derived, so its skill/slash-command tallies
407
+ * were never recorded. This walks the session index, re-parses each transcript
408
+ * FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
409
+ * claude/codex re-derive their tallies from scratch), writes the usage, and
410
+ * stamps resource_scan_ledger so reruns skip completed transcripts — the same
411
+ * independent-ledger shape `agents sessions backfill tools` uses.
412
+ *
413
+ * Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
414
+ * every agent uniformly, so there is no per-harness branch to keep in parity.
415
+ * Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
416
+ */
417
+ export declare function backfillResourceUsage(filter?: QueryOptions, onProgress?: (done: number, total: number) => void): ResourceBackfillResult;
327
418
  /** Who spawned a team: the orchestrator session, from its transcript. */
328
419
  export interface TeamSpawner {
329
420
  sessionId: string;