@phnx-labs/agents-cli 1.22.57 → 1.22.59

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 (152) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/README.md +29 -0
  3. package/dist/bootstrap.js +39 -1
  4. package/dist/commands/accounts.js +7 -3
  5. package/dist/commands/apply.js +10 -2
  6. package/dist/commands/fork.d.ts +23 -10
  7. package/dist/commands/fork.js +115 -58
  8. package/dist/commands/monitors.js +198 -23
  9. package/dist/commands/prune.js +5 -3
  10. package/dist/commands/routines.d.ts +8 -0
  11. package/dist/commands/routines.js +57 -3
  12. package/dist/commands/routines.test-fixture.js +5 -0
  13. package/dist/commands/send.d.ts +2 -1
  14. package/dist/commands/send.js +7 -5
  15. package/dist/commands/sessions-picker.d.ts +11 -0
  16. package/dist/commands/sessions-picker.js +16 -0
  17. package/dist/commands/sessions-stats.js +37 -5
  18. package/dist/commands/sessions.js +40 -5
  19. package/dist/commands/share.d.ts +14 -0
  20. package/dist/commands/share.js +43 -2
  21. package/dist/commands/ssh.js +12 -1
  22. package/dist/commands/status.js +1 -1
  23. package/dist/commands/sync.js +83 -7
  24. package/dist/commands/traces.js +7 -0
  25. package/dist/commands/versions.js +12 -4
  26. package/dist/commands/view.js +7 -2
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +6 -1
  29. package/dist/lib/account-registry.d.ts +5 -1
  30. package/dist/lib/account-registry.js +47 -14
  31. package/dist/lib/accounting/capacity.d.ts +18 -7
  32. package/dist/lib/accounting/capacity.js +19 -8
  33. package/dist/lib/accounting/usage-sync.d.ts +29 -1
  34. package/dist/lib/accounting/usage-sync.js +76 -2
  35. package/dist/lib/accounting/usage.js +7 -1
  36. package/dist/lib/auth-mint.d.ts +11 -1
  37. package/dist/lib/auth-mint.js +21 -6
  38. package/dist/lib/auto-pull-worker.js +7 -2
  39. package/dist/lib/browser/ipc.d.ts +8 -0
  40. package/dist/lib/browser/ipc.js +87 -0
  41. package/dist/lib/browser/service.d.ts +19 -0
  42. package/dist/lib/browser/service.js +96 -11
  43. package/dist/lib/browser/sessions-list.js +10 -1
  44. package/dist/lib/cloud/rush.d.ts +7 -0
  45. package/dist/lib/cloud/rush.js +29 -1
  46. package/dist/lib/daemon/daemon.d.ts +22 -0
  47. package/dist/lib/daemon/daemon.js +39 -0
  48. package/dist/lib/daemon/runner.d.ts +3 -0
  49. package/dist/lib/daemon/runner.js +86 -45
  50. package/dist/lib/daemon/session-index-service.js +9 -1
  51. package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
  52. package/dist/lib/daemon/usage-sync-service.js +14 -8
  53. package/dist/lib/daemon-services.js +1 -1
  54. package/dist/lib/daemon-ticks.d.ts +15 -0
  55. package/dist/lib/daemon-ticks.js +26 -0
  56. package/dist/lib/device-config.d.ts +5 -1
  57. package/dist/lib/device-config.js +2 -2
  58. package/dist/lib/devices/connect.d.ts +17 -8
  59. package/dist/lib/devices/connect.js +31 -14
  60. package/dist/lib/devices/health.js +5 -1
  61. package/dist/lib/devices/pool.d.ts +25 -2
  62. package/dist/lib/devices/pool.js +32 -2
  63. package/dist/lib/devices/stats-cache.d.ts +0 -6
  64. package/dist/lib/devices/stats-cache.js +2 -9
  65. package/dist/lib/doctor-diff.d.ts +14 -0
  66. package/dist/lib/doctor-diff.js +120 -9
  67. package/dist/lib/fleet/manifest.d.ts +17 -0
  68. package/dist/lib/fleet/manifest.js +26 -0
  69. package/dist/lib/git.d.ts +38 -0
  70. package/dist/lib/git.js +58 -0
  71. package/dist/lib/hooks/install.d.ts +27 -11
  72. package/dist/lib/hooks/install.js +42 -17
  73. package/dist/lib/hosts/ready.d.ts +8 -0
  74. package/dist/lib/hosts/ready.js +13 -2
  75. package/dist/lib/hosts/reconnect.d.ts +52 -203
  76. package/dist/lib/hosts/reconnect.js +64 -284
  77. package/dist/lib/installations/migrate.d.ts +6 -120
  78. package/dist/lib/installations/migrate.js +27 -259
  79. package/dist/lib/installations/shims.d.ts +13 -95
  80. package/dist/lib/installations/shims.js +22 -139
  81. package/dist/lib/installations/store.js +1 -1
  82. package/dist/lib/installations/versions.d.ts +43 -133
  83. package/dist/lib/installations/versions.js +94 -206
  84. package/dist/lib/monitors/config.d.ts +71 -3
  85. package/dist/lib/monitors/config.js +100 -12
  86. package/dist/lib/monitors/pid-watch.d.ts +35 -0
  87. package/dist/lib/monitors/pid-watch.js +45 -0
  88. package/dist/lib/monitors/remote.d.ts +18 -0
  89. package/dist/lib/monitors/remote.js +11 -0
  90. package/dist/lib/permissions.js +7 -2
  91. package/dist/lib/plugins/plugins.d.ts +17 -3
  92. package/dist/lib/plugins/plugins.js +84 -9
  93. package/dist/lib/plugins/skills.d.ts +8 -1
  94. package/dist/lib/plugins/skills.js +18 -2
  95. package/dist/lib/pty-server.d.ts +14 -0
  96. package/dist/lib/pty-server.js +49 -5
  97. package/dist/lib/refresh.d.ts +9 -0
  98. package/dist/lib/refresh.js +3 -1
  99. package/dist/lib/routine-readiness.d.ts +15 -1
  100. package/dist/lib/routine-readiness.js +41 -0
  101. package/dist/lib/sandbox.d.ts +4 -1
  102. package/dist/lib/sandbox.js +30 -1
  103. package/dist/lib/secrets/agent.d.ts +80 -225
  104. package/dist/lib/secrets/agent.js +139 -401
  105. package/dist/lib/secrets/bundles.d.ts +73 -222
  106. package/dist/lib/secrets/bundles.js +168 -467
  107. package/dist/lib/secrets/drivers/rush.js +5 -0
  108. package/dist/lib/secrets/reaper.d.ts +28 -70
  109. package/dist/lib/secrets/reaper.js +30 -85
  110. package/dist/lib/secrets/remote.d.ts +42 -129
  111. package/dist/lib/secrets/remote.js +55 -173
  112. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  113. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  114. package/dist/lib/self-heal/registry.js +2 -0
  115. package/dist/lib/self-heal/types.d.ts +1 -1
  116. package/dist/lib/self-update.d.ts +65 -0
  117. package/dist/lib/self-update.js +138 -0
  118. package/dist/lib/session/active.d.ts +13 -1
  119. package/dist/lib/session/active.js +2 -0
  120. package/dist/lib/session/cloud.js +5 -0
  121. package/dist/lib/session/db.d.ts +51 -6
  122. package/dist/lib/session/db.js +266 -20
  123. package/dist/lib/session/fork.d.ts +45 -26
  124. package/dist/lib/session/fork.js +32 -95
  125. package/dist/lib/session/tool-calls.d.ts +43 -1
  126. package/dist/lib/session/tool-calls.js +74 -44
  127. package/dist/lib/session/tool-store.d.ts +33 -2
  128. package/dist/lib/session/tool-store.js +56 -3
  129. package/dist/lib/smart-launch.d.ts +6 -0
  130. package/dist/lib/smart-launch.js +5 -2
  131. package/dist/lib/staleness/writers/plugins.js +5 -2
  132. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  133. package/dist/lib/staleness/writers/sources.js +2 -1
  134. package/dist/lib/staleness/writers/subagents.js +13 -3
  135. package/dist/lib/state.d.ts +7 -4
  136. package/dist/lib/state.js +7 -4
  137. package/dist/lib/subagents.js +8 -2
  138. package/dist/lib/sync-status.d.ts +22 -0
  139. package/dist/lib/sync-status.js +27 -0
  140. package/dist/lib/sync-umbrella.d.ts +9 -0
  141. package/dist/lib/sync-umbrella.js +21 -2
  142. package/dist/lib/teams/scheduler.d.ts +10 -0
  143. package/dist/lib/teams/scheduler.js +8 -0
  144. package/dist/lib/traces/insights.d.ts +47 -14
  145. package/dist/lib/traces/insights.js +92 -21
  146. package/dist/lib/traces/phenotype.d.ts +23 -3
  147. package/dist/lib/traces/phenotype.js +72 -24
  148. package/dist/lib/traces/sync.d.ts +128 -6
  149. package/dist/lib/traces/sync.js +294 -35
  150. package/dist/lib/traces/worker-template.js +154 -1
  151. package/dist/lib/view-types.d.ts +12 -0
  152. package/package.json +2 -2
@@ -292,6 +292,56 @@ export function deriveGlobalPrefix(packageRoot) {
292
292
  const parent = path.dirname(nodeModulesDir);
293
293
  return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
294
294
  }
295
+ /**
296
+ * Sweep npm arborist's "retired" staging dir for `packageRoot` before a
297
+ * reify (PHNX-3393).
298
+ *
299
+ * npm (@npmcli/arborist) reifies an install by first renaming the tree it is
300
+ * about to replace out of the way into a sibling directory —
301
+ * `retirePath(from)` in arborist's own source names it
302
+ * `.<basename>-<8-char sha1 hash of the full path>`, sibling to `from` — then
303
+ * stages the new tree and renames it into place. Because the hash is a pure
304
+ * function of `packageRoot`'s path, that staging path is IDENTICAL on every
305
+ * reify of this install. A crash between the retire-rename and the final
306
+ * rename (SIGKILL, a killed terminal, a box that lost power mid-upgrade)
307
+ * leaves that exact directory behind, non-empty. `rename(2)` cannot replace a
308
+ * non-empty directory, so every subsequent upgrade's reify fails ENOTEMPTY at
309
+ * the same path forever — nothing about a plain retry ever clears it.
310
+ *
311
+ * Removing any stale `.<basename>-*` sibling before install self-heals this:
312
+ * npm re-stages cleanly once the collision is gone. Matches only the
313
+ * retire-path shape (a dot-prefixed sibling starting with the package's own
314
+ * basename), so an unrelated dotfile in the same directory is left alone.
315
+ * Best-effort per entry: one unremovable sibling must not block the rest.
316
+ */
317
+ export function sweepStaleInstallStaging(packageRoot) {
318
+ const resolved = path.resolve(packageRoot);
319
+ const dir = path.dirname(resolved);
320
+ const base = path.basename(resolved);
321
+ const escapedBase = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
322
+ const stagingPattern = new RegExp(`^\\.${escapedBase}-[a-zA-Z0-9]+$`);
323
+ let entries;
324
+ try {
325
+ entries = fs.readdirSync(dir);
326
+ }
327
+ catch {
328
+ return [];
329
+ }
330
+ const swept = [];
331
+ for (const entry of entries) {
332
+ if (!stagingPattern.test(entry))
333
+ continue;
334
+ const full = path.join(dir, entry);
335
+ try {
336
+ fs.rmSync(full, { recursive: true, force: true });
337
+ swept.push(full);
338
+ }
339
+ catch {
340
+ /* best-effort — one unremovable stager must not block the rest */
341
+ }
342
+ }
343
+ return swept;
344
+ }
295
345
  /**
296
346
  * Install `spec` into an explicit global prefix. `--prefix` pins the
297
347
  * destination no matter which npm binary PATH resolves. `--ignore-scripts`
@@ -404,6 +454,94 @@ export function refreshAliasShims(packageRoot) {
404
454
  stdio: 'ignore',
405
455
  });
406
456
  }
457
+ /** Resolve `p` through symlinks, or null when it does not resolve (missing/dangling). */
458
+ function realpathOrNull(p) {
459
+ try {
460
+ return fs.realpathSync(p);
461
+ }
462
+ catch {
463
+ return null;
464
+ }
465
+ }
466
+ /**
467
+ * Reconcile one `<binDir>/<name>` link to `target`. A link that already resolves
468
+ * to `target` is left untouched (`ok`); anything else — absent, dangling, or
469
+ * pointing at a stale/foreign path — is replaced with a fresh **relative**
470
+ * symlink (`../lib/node_modules/@phnx-labs/agents-cli/dist/index.js`), the exact
471
+ * shape npm and the by-hand zion repair both produced, then re-verified. A
472
+ * repair that still does not resolve (target missing, unwritable bin dir) is
473
+ * reported `failed` with the reason rather than silently swallowed.
474
+ */
475
+ function reconcileBinLink(name, linkPath, target) {
476
+ const wanted = realpathOrNull(target);
477
+ if (wanted !== null && realpathOrNull(linkPath) === wanted) {
478
+ return { name, linkPath, target, action: 'ok' };
479
+ }
480
+ try {
481
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
482
+ // Replace whatever is there (a dangling link, a stale link, or nothing).
483
+ fs.rmSync(linkPath, { force: true });
484
+ fs.symlinkSync(path.relative(path.dirname(linkPath), target), linkPath);
485
+ const resolved = realpathOrNull(linkPath);
486
+ if (resolved !== null && resolved === realpathOrNull(target)) {
487
+ return { name, linkPath, target, action: 'repaired' };
488
+ }
489
+ return {
490
+ name,
491
+ linkPath,
492
+ target,
493
+ action: 'failed',
494
+ error: resolved === null
495
+ ? `link created but still does not resolve (is ${target} present?)`
496
+ : `link resolves to ${resolved}, not ${target}`,
497
+ };
498
+ }
499
+ catch (err) {
500
+ return { name, linkPath, target, action: 'failed', error: err instanceof Error ? err.message : String(err) };
501
+ }
502
+ }
503
+ /**
504
+ * The global bin links the upgrade OWNS: `<prefix>/bin/<name>` for every
505
+ * `package.json#bin` entry (`agents`, `ag`, `browser`, `computer`).
506
+ *
507
+ * npm creates these on a normal `install -g`, but a box with several installs
508
+ * (or a reify interrupted after the old links were retired) can end an upgrade
509
+ * with the package at the new version and these links **missing** — the state
510
+ * that stranded zion (PHNX-2768): `/opt/homebrew/lib/node_modules/...` at
511
+ * 1.22.40 but `/opt/homebrew/bin/{agents,ag,browser,computer}` gone, so every
512
+ * `agents` invocation was "command not found" until the links were relinked by
513
+ * hand. The rollout probe reported it `unverified`; the box was left broken.
514
+ *
515
+ * So the upgrade verifies these links right after installing and **restores any
516
+ * that are wrong** — covering the sibling entrypoints, not just `agents`, since
517
+ * `ag`/`browser`/`computer` share the same failure. Each link is reconciled
518
+ * independently; a link that cannot be made to resolve is reported `failed` so
519
+ * the caller can fail loud (the rollout then marks the box failed, not merely
520
+ * unverified) instead of returning a box the package upgraded but cannot run.
521
+ *
522
+ * POSIX only — Windows npm bins are `.cmd`/`.ps1` shims, not symlinks, so this
523
+ * relink shape does not apply and the caller skips it there. `prefix` is the
524
+ * npm global prefix from {@link deriveGlobalPrefix}; the bun path uses its own
525
+ * bin layout and is out of scope.
526
+ */
527
+ export function ensureGlobalBinLinks(packageRoot, prefix) {
528
+ let bin;
529
+ try {
530
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
531
+ bin = pkg && typeof pkg.bin === 'object' && pkg.bin !== null ? pkg.bin : {};
532
+ }
533
+ catch (err) {
534
+ throw new Error(`could not read bin entries from ${path.join(packageRoot, 'package.json')}: ${err instanceof Error ? err.message : String(err)}`);
535
+ }
536
+ const binDir = path.join(prefix, 'bin');
537
+ const repairs = [];
538
+ for (const [name, rel] of Object.entries(bin)) {
539
+ if (typeof rel !== 'string' || !rel)
540
+ continue;
541
+ repairs.push(reconcileBinLink(name, path.join(binDir, name), path.resolve(packageRoot, rel)));
542
+ }
543
+ return repairs;
544
+ }
407
545
  /**
408
546
  * The package root a resolved `agents` entrypoint belongs to, or null when the
409
547
  * path is not an agents-cli entry at all. Two shipped shapes:
@@ -55,7 +55,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
55
55
  export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
56
56
  type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
57
57
  /** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
58
- export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
58
+ export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
59
59
  export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
60
60
  export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
61
61
  export declare function isRunningLiveSession(s: ActiveSession): boolean;
@@ -201,6 +201,18 @@ export interface ActiveSession {
201
201
  * id (RUSH-2205), never asserted by a source.
202
202
  */
203
203
  version?: string;
204
+ /**
205
+ * Email of the account that produced the session (display-only). Like
206
+ * {@link version}, a running process does not report which account a
207
+ * `--strategy balanced` launch selected, so it is backfilled at render time
208
+ * from the indexed {@link SessionMeta} by session id (PHNX-3184). This is what
209
+ * the AGI EXT status bar renders as the session's account — it reads it off the
210
+ * `sessions watch --json` row instead of spawning a per-tab `agents sessions
211
+ * <id> --device <host> --json` (the 2026-08-25 CPU incident, agi-cli#3019).
212
+ * Never group on this — two orgs can share one email; group on the index's
213
+ * `accountKey`.
214
+ */
215
+ account?: string;
204
216
  /**
205
217
  * Last-activity epoch — the transcript's last write (mtime). Distinct from
206
218
  * {@link startedAtMs} (session START): a session begun 3h ago but last touched
@@ -145,6 +145,8 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
145
145
  continue;
146
146
  if (!s.version && m.version)
147
147
  s.version = m.version;
148
+ if (!s.account && m.account)
149
+ s.account = m.account;
148
150
  if (!s.label && m.label)
149
151
  s.label = m.label;
150
152
  if (!s.ticket && m.ticketId)
@@ -29,6 +29,11 @@ function readToken() {
29
29
  if (!token) {
30
30
  throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
31
31
  }
32
+ const expiresAt = data.session?.expires_at;
33
+ if (typeof expiresAt === 'number' && expiresAt <= Date.now() / 1000) {
34
+ const expiredAt = new Date(expiresAt * 1000).toISOString();
35
+ throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
36
+ }
32
37
  return token;
33
38
  }
34
39
  async function api(method, endpoint, token) {
@@ -9,10 +9,11 @@
9
9
  import Database from '../sqlite.js';
10
10
  import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
11
11
  import { type IndexedToolCall } from './tool-calls.js';
12
+ import { type ToolScanResumePoint } from './tool-store.js';
12
13
  /** Current schema version; bumped when migrations are added. Exported so tests
13
14
  * assert against the constant instead of hardcoding a number that every bump
14
15
  * then has to chase (docs/sessions.md calls the constant the source of truth). */
15
- export declare const SCHEMA_VERSION = 42;
16
+ export declare const SCHEMA_VERSION = 44;
16
17
  /**
17
18
  * Bump to force the content extractor (assistant-answer text, alongside the
18
19
  * user-prompt text every harness already accumulates) to re-derive on every
@@ -35,6 +36,8 @@ export declare const CONTENT_INDEX_VERSION = 1;
35
36
  export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
36
37
  /** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
37
38
  export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
39
+ /** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
40
+ export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
38
41
  /** File stat snapshot used to detect changes between scan runs. */
39
42
  export interface ScanStamp {
40
43
  fileMtimeMs: number;
@@ -249,6 +252,13 @@ export declare function upsertSessionsBatch(entries: Array<{
249
252
  toolCalls?: IndexedToolCall[];
250
253
  toolScan?: ScanStamp;
251
254
  toolIndexMode?: 'replace' | 'append';
255
+ /**
256
+ * Where the NEXT scan of this full-file-harness session may resume its tool
257
+ * index (PHNX-3411). Computed internally in the enrichment map below; not
258
+ * supplied by callers. Persisted alongside the tool ledger so an active
259
+ * session re-derives only its newly appended tool calls next tick.
260
+ */
261
+ toolResume?: ToolScanResumePoint | null;
252
262
  }>): void;
253
263
  /**
254
264
  * Sync labels for a set of sessions. For each id in the map, if the stored
@@ -307,6 +317,16 @@ export declare function getSessionExistenceCacheStats(): {
307
317
  };
308
318
  /** Query sessions from the database, applying filters and ordering by last-activity descending (default). */
309
319
  export declare function querySessions(options?: QueryOptions): SessionMeta[];
320
+ /**
321
+ * Cheap query for the daemon's deferred tool-index pass (PHNX-3411).
322
+ *
323
+ * Returns the most-recently-active sessions whose parseSession reads a large
324
+ * flat transcript (kimi: wire.jsonl, grok: chat_history.jsonl). Their scanners
325
+ * produce no events, so upsertSessionsBatch skips them in the warm tick to
326
+ * avoid wedging the event loop. ensureToolIndex uses tool_scan_ledger stamps
327
+ * to skip already-current rows and applies byte/file budget caps.
328
+ */
329
+ export declare function querySessionsForDeferredToolIndex(limit: number): SessionMeta[];
310
330
  /** Count sessions matching the given filter options. */
311
331
  export declare function countSessions(options?: QueryOptions): number;
312
332
  /** One grouped row in a cost/duration rollup. */
@@ -377,6 +397,15 @@ export declare function writeSessionTopics<T>(entries: Array<{
377
397
  fileSize: number | null;
378
398
  topic: T;
379
399
  }>): void;
400
+ /** Read cached failure phenotypes only when their transcript byte stamps still match. */
401
+ export declare function readSessionPhenotypes<T>(ids: string[]): Map<string, T>;
402
+ /** Persist failure phenotypes against the exact transcript bytes used to classify them. */
403
+ export declare function writeSessionPhenotypes<T>(entries: Array<{
404
+ id: string;
405
+ fileMtimeMs: number | null;
406
+ fileSize: number | null;
407
+ phenotype: T;
408
+ }>): void;
380
409
  /** Read one derived preview only when it matches the transcript bytes on disk. */
381
410
  export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
382
411
  fileMtimeMs: number | null;
@@ -500,14 +529,30 @@ export declare function queryResourceUsageStats(options: QueryOptions & {
500
529
  limit?: number;
501
530
  }): ResourceStatRow[];
502
531
  /**
503
- * Coverage of the resource-usage signal: how many distinct sessions carry any
504
- * row in session_resource_usage vs. the total indexed. A low ratio means the
505
- * historical backfill (`agents sessions backfill resources`) hasn't run — the
506
- * stats surface uses this to tell the user their zero-counts may just be
507
- * un-scanned history, not genuine non-use.
532
+ * Coverage of the resource-usage signal, as three honest facts:
533
+ *
534
+ * - `scanned` — sessions the resource extractor has actually processed, i.e.
535
+ * those carrying a `resource_scan_ledger` row at the current
536
+ * `RESOURCE_INDEX_VERSION`. This is the true "has the historical backfill run"
537
+ * signal: the ledger is stamped for EVERY scanned session, including ones that
538
+ * invoked nothing (`resource_count = 0`), so `scanned/total` rises to ~1 after
539
+ * `agents sessions backfill resources` regardless of how sparse explicit
540
+ * invocations are.
541
+ * - `covered` — distinct sessions that carry AT LEAST ONE row in
542
+ * `session_resource_usage`, i.e. that actually recorded an explicit invocation.
543
+ * This is an ABSOLUTE signal count, not a coverage ratio: it stays small even
544
+ * at full scan coverage because most sessions invoke no skill/command, and a
545
+ * non-recording harness contributes none by construction.
546
+ * - `total` — sessions indexed.
547
+ *
548
+ * The two were previously conflated: `covered/total` was framed as coverage and
549
+ * read ~1.2% even after a full backfill (most sessions genuinely invoke nothing),
550
+ * so the "run the backfill" hint never cleared. Keying the hint on `scanned/total`
551
+ * fixes that — see `commands/sessions-stats.ts` (PHNX-2301).
508
552
  */
509
553
  export declare function resourceUsageCoverage(): {
510
554
  covered: number;
555
+ scanned: number;
511
556
  total: number;
512
557
  };
513
558
  /** Outcome of a resource-usage backfill run. */