disk-space-saviour 0.22.0 → 0.23.0

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/src/env/local.js CHANGED
@@ -100,6 +100,7 @@ import { hostExecutor } from '../exec.js';
100
100
  import { parseBundleIdentifier } from '../app-owners.js';
101
101
  import { matchesGlob } from '../paths.js';
102
102
  import { mapLimit } from './parallel.js';
103
+ import { withLinkAccounting } from './hardlinks.js';
103
104
  import { directoryFingerprint, SIZE_MAX_AGE } from '../scan-state.js';
104
105
  import { indexedDirectories } from './platform-index.js';
105
106
 
@@ -213,6 +214,15 @@ async function makeWritable(target) {
213
214
  }
214
215
  }
215
216
 
217
+ function linkOf(stats, file) {
218
+ return {
219
+ path: file,
220
+ id: `${stats.dev}:${stats.ino}`,
221
+ nlink: stats.nlink,
222
+ bytes: diskBytes(stats),
223
+ };
224
+ }
225
+
216
226
  export class LocalEnv {
217
227
  /**
218
228
  * @param {{homes?: string[], tmpDirs?: string[], executor?: object,
@@ -322,6 +332,7 @@ export class LocalEnv {
322
332
  mtimeMs: stats.mtimeMs,
323
333
  ctimeMs: stats.ctimeMs,
324
334
  inode: `${stats.dev}:${stats.ino}`,
335
+ nlink: stats.nlink,
325
336
  uid: stats.uid,
326
337
  gid: stats.gid,
327
338
  mode: stats.mode,
@@ -440,14 +451,16 @@ export class LocalEnv {
440
451
  return null;
441
452
  }
442
453
  const seen = new Set();
454
+ const links = [];
443
455
  let bytes = 0;
444
456
  let apparentBytes = 0;
445
457
  let files = 0;
446
458
  let errors = 0;
447
459
  let newestMtimeMs = 0;
448
- const account = (stats) => {
460
+ const account = (stats, full) => {
449
461
  if (stats.nlink > 1 && !stats.isDirectory()) {
450
462
  const key = `${stats.dev}:${stats.ino}`;
463
+ links.push(linkOf(stats, full));
451
464
  if (seen.has(key)) {
452
465
  return;
453
466
  }
@@ -463,7 +476,7 @@ export class LocalEnv {
463
476
  newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
464
477
  }
465
478
  };
466
- account(root);
479
+ account(root, target);
467
480
  const visit = async (dir) => {
468
481
  let names;
469
482
  try {
@@ -489,7 +502,7 @@ export class LocalEnv {
489
502
  ) {
490
503
  continue;
491
504
  }
492
- account(stats);
505
+ account(stats, full);
493
506
  if (stats.isDirectory()) {
494
507
  subdirs.push(full);
495
508
  }
@@ -511,7 +524,10 @@ export class LocalEnv {
511
524
  if (files === 0) {
512
525
  newestMtimeMs = root.mtimeMs;
513
526
  }
514
- return { bytes, apparentBytes, files, errors, newestMtimeMs };
527
+ return withLinkAccounting(
528
+ { bytes, apparentBytes, files, errors, newestMtimeMs },
529
+ links
530
+ );
515
531
  }
516
532
 
517
533
  /**
@@ -734,6 +750,29 @@ export class LocalEnv {
734
750
  return usage;
735
751
  }
736
752
 
753
+ /** Multi-link files below `target`, which `du` cannot attribute. */
754
+ async #multiLinks(target) {
755
+ const result = await this.run([
756
+ 'find',
757
+ target,
758
+ '-xdev',
759
+ '!',
760
+ '-type',
761
+ 'd',
762
+ '-links',
763
+ '+1',
764
+ '-print0',
765
+ ]);
766
+ const files = result.stdout.split('\0').filter(Boolean);
767
+ const stats = await mapLimit(files, 32, async (file) => [
768
+ file,
769
+ await this.#lstat(file),
770
+ ]);
771
+ return stats
772
+ .filter(([, entry]) => entry)
773
+ .map(([file, entry]) => linkOf(entry, file));
774
+ }
775
+
737
776
  async quickUsage(target) {
738
777
  const stats = await this.stat(target);
739
778
  if (!stats) {
@@ -755,13 +794,16 @@ export class LocalEnv {
755
794
  if (result.code !== 0 || !match) {
756
795
  return this.rawUsage(target);
757
796
  }
758
- const usage = {
759
- bytes: Number(match[1]) * 1024,
760
- apparentBytes: Number(match[1]) * 1024,
761
- newestMtimeMs: 0,
762
- files: 0,
763
- errors: 0,
764
- };
797
+ const usage = withLinkAccounting(
798
+ {
799
+ bytes: Number(match[1]) * 1024,
800
+ apparentBytes: Number(match[1]) * 1024,
801
+ newestMtimeMs: 0,
802
+ files: 0,
803
+ errors: 0,
804
+ },
805
+ await this.#multiLinks(target)
806
+ );
765
807
  if (this.scanMetrics) {
766
808
  this.scanMetrics.directoriesSized++;
767
809
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Effective scan roots of one environment and the scope report that makes
3
+ * them visible.
4
+ *
5
+ * Root semantics (also documented in README "Scan roots"):
6
+ * - host: `roots` (positional paths) or, when none, the homes and temporary
7
+ * directories;
8
+ * - container (every Docker level): `containerRoots` replaces the defaults
9
+ * when given; otherwise the defaults (homes, temporary directories and
10
+ * the common workspace paths in CONTAINER_ROOTS that exist) plus every
11
+ * host `roots` path that also exists inside that container. Host and
12
+ * container namespaces differ, so a host root absent from a container is
13
+ * reported as `absent`; paths are never guessed or remapped.
14
+ * The same requested roots reach the traversal policy (mount selection)
15
+ * and project discovery, so both always agree.
16
+ */
17
+ import { existingPaths, matchesExcluded } from '../paths.js';
18
+
19
+ /** Common project locations inside containers, scanned when present. */
20
+ export const CONTAINER_ROOTS = [
21
+ '/workspace',
22
+ '/workspaces',
23
+ '/app',
24
+ '/src',
25
+ '/code',
26
+ '/project',
27
+ '/usr/src/app',
28
+ ];
29
+
30
+ /** Roots explicitly requested for `env` (null when only defaults apply). */
31
+ export function requestedRoots(env, options, overrides = {}) {
32
+ if (overrides.roots) {
33
+ return overrides.roots;
34
+ }
35
+ if (env.kind === 'container') {
36
+ return options.containerRoots ?? options.roots ?? null;
37
+ }
38
+ return options.roots ?? null;
39
+ }
40
+
41
+ const unique = (list) => [...new Set(list)];
42
+
43
+ /**
44
+ * Resolves the roots project discovery walks plus one scope entry per
45
+ * considered root.
46
+ * @returns {Promise<{roots: string[], entries: object[]}>}
47
+ */
48
+ export async function resolveRoots(env, options, overrides, homes, tmpDirs) {
49
+ const entries = [];
50
+ const add = (path, source) => {
51
+ if (!entries.some((entry) => entry.path === path)) {
52
+ entries.push({ path, source });
53
+ }
54
+ };
55
+ if (overrides.roots) {
56
+ overrides.roots.forEach((root) => add(root, 'roots'));
57
+ } else if (env.kind === 'container' && options.containerRoots) {
58
+ options.containerRoots.forEach((root) => add(root, 'container-roots'));
59
+ } else if (env.kind !== 'container' && options.roots) {
60
+ options.roots.forEach((root) => add(root, 'roots'));
61
+ } else {
62
+ homes.forEach((root) => add(root, 'home'));
63
+ tmpDirs.forEach((root) => add(root, 'tmp'));
64
+ if (env.kind === 'container') {
65
+ const present = await existingPaths(env, CONTAINER_ROOTS);
66
+ CONTAINER_ROOTS.filter((root) => present.has(root)).forEach((root) =>
67
+ add(root, 'container-default')
68
+ );
69
+ (options.roots ?? []).forEach((root) => add(root, 'roots'));
70
+ }
71
+ }
72
+ await classify(env, options, entries);
73
+ return {
74
+ roots: unique(
75
+ entries.filter((entry) => entry.status === 'pending').map((e) => e.path)
76
+ ),
77
+ entries,
78
+ };
79
+ }
80
+
81
+ async function classify(env, options, entries) {
82
+ const policy = env.scanPolicy;
83
+ const allowed = entries.filter((entry) => {
84
+ if (matchesExcluded(entry.path, options.exclude ?? [], env.path)) {
85
+ entry.status = 'excluded';
86
+ return false;
87
+ }
88
+ if (policy && !policy.allows(entry.path)) {
89
+ entry.status = 'pruned';
90
+ return false;
91
+ }
92
+ return true;
93
+ });
94
+ const present = allowed.length
95
+ ? await existingPaths(
96
+ env,
97
+ allowed.map((entry) => entry.path)
98
+ )
99
+ : new Set();
100
+ for (const entry of allowed) {
101
+ entry.status = present.has(entry.path) ? 'pending' : 'absent';
102
+ }
103
+ }
104
+
105
+ /** Scope descriptor stored on the environment and copied into reports. */
106
+ export function scanScope(env, options, entries) {
107
+ return {
108
+ roots: entries.map((entry) => ({ ...entry })),
109
+ maxDepth: options.maxDepth,
110
+ exclude: [...(options.exclude ?? [])],
111
+ prunedMounts: [...(env.scanPolicy?.pruned ?? [])],
112
+ note:
113
+ 'scanStatus "complete" means complete within these roots, maxDepth, ' +
114
+ 'exclusions and mount boundaries; fixed cache, toolchain and agent ' +
115
+ 'locations are checked independently of roots',
116
+ };
117
+ }
118
+
119
+ /** Final per-root status when the scanners of the environment finish. */
120
+ export function finishScope(env, ranProjects) {
121
+ const scope = env.scanScope;
122
+ if (!scope) {
123
+ return;
124
+ }
125
+ const denied = new Set(env.accessErrors?.keys() ?? []);
126
+ for (const root of scope.roots) {
127
+ if (root.status !== 'pending') {
128
+ continue;
129
+ }
130
+ if (denied.has(root.path)) {
131
+ root.status = 'inaccessible';
132
+ } else if (!ranProjects) {
133
+ root.status = 'not-searched';
134
+ } else {
135
+ root.status = env.scanStatus === 'complete' ? 'searched' : 'partial';
136
+ }
137
+ }
138
+ }
package/src/env/shell.js CHANGED
@@ -20,6 +20,7 @@ import { sharedWritableLayer } from '../docker/writable.js';
20
20
  import { containerExecutor, shellQuote, trace } from '../exec.js';
21
21
  import { DEFAULT_SKIP_NAMES, parseDf, processAliases } from './local.js';
22
22
  import { batches } from './batches.js';
23
+ import { LINKS_SCRIPT, parseLinks, withLinkAccounting } from './hardlinks.js';
23
24
  import { signalExecutor } from './scope.js';
24
25
 
25
26
  const STAT_FORMAT = '%F|%s|%b|%B|%Y|%n';
@@ -44,6 +45,8 @@ const USAGE_MANY_SCRIPT = `for t in "$@"; do
44
45
  du -skx -- "$t" 2>/dev/null | cut -f1
45
46
  find "$t" -xdev ! -type d -exec stat -c %Y {} + 2>/dev/null | awk 'BEGIN{m=0;n=0}{n++; if ($1>m) m=$1} END{print n, m}'
46
47
  stat -c %Y -- "$t"
48
+ echo links
49
+ ${LINKS_SCRIPT}
47
50
  done
48
51
  true`;
49
52
 
@@ -120,10 +123,18 @@ function splitRecords(output) {
120
123
  }
121
124
 
122
125
  function parseUsage(body) {
123
- const [kib, counts = '', rootMtime] = body.trim().split('\n');
126
+ const [head, links = ''] = body.split('\nlinks\n');
127
+ const [kib, counts = '', rootMtime] = head.trim().split('\n');
124
128
  const [files, newest] = counts.trim().split(/\s+/).map(Number);
125
129
  const bytes = Number(kib) * 1024;
126
130
  const newestSeconds = files > 0 ? newest : Number(rootMtime);
131
+ return withLinkAccounting(
132
+ usageFields(bytes, files, newestSeconds),
133
+ parseLinks(links)
134
+ );
135
+ }
136
+
137
+ function usageFields(bytes, files, newestSeconds) {
127
138
  return {
128
139
  bytes: Number.isFinite(bytes) ? bytes : 0,
129
140
  apparentBytes: Number.isFinite(bytes) ? bytes : 0,
@@ -292,7 +303,7 @@ export class ShellEnv {
292
303
  const result = await this.executor.run([
293
304
  'stat',
294
305
  '-c',
295
- `${STAT_FORMAT}|%d:%i:%u:%g:%f`,
306
+ `${STAT_FORMAT}|%d:%i:%u:%g:%f:%h`,
296
307
  '--',
297
308
  ...batch,
298
309
  ]);
@@ -310,6 +321,7 @@ export class ShellEnv {
310
321
  identity.length > 4
311
322
  ? Number.parseInt(identity[4], 16)
312
323
  : undefined,
324
+ nlink: identity.length > 5 ? Number(identity[5]) : undefined,
313
325
  });
314
326
  }
315
327
  }
package/src/index.d.ts CHANGED
@@ -9,6 +9,13 @@ export type Duration = number | string;
9
9
  export interface ScanOptions {
10
10
  /** Directories to search for projects (default: homes and tmp dirs). */
11
11
  roots?: string[] | string | null;
12
+ /**
13
+ * Project roots inside every scanned container (all Docker depths).
14
+ * Replaces the container defaults; without it containers search their
15
+ * homes, temporary directories, common workspace paths and every `roots`
16
+ * path that exists inside them.
17
+ */
18
+ containerRoots?: string[] | string | null;
12
19
  /** Directory depth for project search. */
13
20
  maxDepth?: number;
14
21
  /** Time budget per filesystem environment (`2m`); incomplete findings are kept. */
@@ -117,7 +124,15 @@ export interface Item {
117
124
  description: string;
118
125
  path: string | null;
119
126
  paths: string[];
127
+ /**
128
+ * Physically reclaimable bytes: blocks of hard-linked files with a link
129
+ * outside the item are not counted.
130
+ */
120
131
  bytes: number;
132
+ /** Blocks reachable from the item (each inode once); set with sharedBytes. */
133
+ allocatedBytes?: number;
134
+ /** Blocks retained by hard links outside the item; present when non-zero. */
135
+ sharedBytes?: number;
121
136
  /** Cached size is advisory; cleanup always measures again. */
122
137
  sizeEstimated?: boolean;
123
138
  /** Merged filesystem bytes, including image data, when measured in Docker. */
@@ -180,6 +195,8 @@ export interface EnvironmentDescriptor {
180
195
  swappedBytes?: number;
181
196
  /** Change since the earliest scan of the last week listing this process. */
182
197
  growth?: { since: number; memoryBytes: number };
198
+ /** The dss process itself, never named as one to quit. */
199
+ self?: boolean;
183
200
  }>;
184
201
  /** Change since the earliest scan of the last week. */
185
202
  growth?: { since: number; usedBytes: number } | null;
@@ -196,9 +213,32 @@ export interface EnvironmentDescriptor {
196
213
  | 'budget-exceeded'
197
214
  | 'error'
198
215
  | null;
216
+ /** What `scanStatus: complete` covers in this environment. */
217
+ scope?: ScanScope | null;
199
218
  layerSource?: string | null;
200
219
  }
201
220
 
221
+ export interface ScanScope {
222
+ roots: Array<{
223
+ path: string;
224
+ source: 'roots' | 'container-roots' | 'home' | 'tmp' | 'container-default';
225
+ status:
226
+ | 'searched'
227
+ | 'partial'
228
+ | 'absent'
229
+ | 'inaccessible'
230
+ | 'pruned'
231
+ | 'excluded'
232
+ | 'not-searched'
233
+ | 'pending';
234
+ }>;
235
+ maxDepth: number;
236
+ exclude: string[];
237
+ /** Mount points never traversed (unselected, read-only or overlay). */
238
+ prunedMounts: string[];
239
+ note: string;
240
+ }
241
+
202
242
  export interface TierTotal {
203
243
  items: number;
204
244
  bytes: number;
@@ -258,6 +298,10 @@ export interface AuditEntry {
258
298
  freedBytes: number;
259
299
  /** True when authoritative before/after allocation could not be measured. */
260
300
  freedEstimated?: boolean;
301
+ /** `'hardlink-aware-estimate'`: freedBytes is the link-aware selection estimate. */
302
+ freedAccounting?: 'hardlink-aware-estimate';
303
+ /** Blocks of deleted files that stay allocated through outside hard links. */
304
+ sharedBytes?: number;
261
305
  durationMs: number;
262
306
  backup?: string;
263
307
  [key: string]: unknown;
@@ -288,6 +332,8 @@ export interface AuditLog {
288
332
  plannedBytes: number;
289
333
  environments: Record<string, EnvironmentTotal>;
290
334
  file?: string;
335
+ /** Set while the disk was too full to write the log; deletion went on. */
336
+ auditError?: { message: string; code: string };
291
337
  goal?: { freeBytes: number | null; untilPercent: number | null };
292
338
  goalMet?: boolean;
293
339
  reachedTier?: Tier;
package/src/liveness.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * - a command argument names a path inside the item (including scripts
11
11
  * whose interpreter has already closed its input descriptor);
12
12
  * - a directory inside the item is on a process's `PATH`, so the process
13
- * may start a program from it at any time.
13
+ * may start a program from it at any time;
14
+ * - the path lies in the scratchpad of a Claude Code session that was active
15
+ * inside the inactive window, so the session may still rely on it.
14
16
  *
15
17
  * The cleaner refreshes the probe right before acting on each item.
16
18
  */
@@ -27,6 +29,32 @@ import { isWithin } from './paths.js';
27
29
  import { formatDuration } from './units.js';
28
30
 
29
31
  const PROC_COMM_LIMIT = 15;
32
+ const DEFAULT_INACTIVE_MS = 30 * 24 * 60 * 60 * 1000;
33
+ const SESSION_ID =
34
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
35
+
36
+ /**
37
+ * The Claude Code session directory a path lies in: Claude Code keeps each
38
+ * session's scratchpad under `<tmp>/claude-<uid>/<project>/<session id>`.
39
+ * @returns {{dir: string, project: string, id: string}|null}
40
+ */
41
+ export function agentSessionOf(target, pathApi = path) {
42
+ const parts = target.split(/[\\/]/);
43
+ const index = parts.findIndex(
44
+ (part, at) =>
45
+ /^claude-\d+$/.test(part) &&
46
+ Boolean(parts[at + 1]) &&
47
+ SESSION_ID.test(parts[at + 2] ?? '')
48
+ );
49
+ if (index < 0) {
50
+ return null;
51
+ }
52
+ return {
53
+ dir: parts.slice(0, index + 3).join(pathApi.sep),
54
+ project: parts[index + 1],
55
+ id: parts[index + 2],
56
+ };
57
+ }
30
58
 
31
59
  /** Bun subcommands that read or write the install cache. */
32
60
  const BUN_PACKAGE_COMMANDS = new Set([
@@ -210,11 +238,21 @@ function appLabel(proc, matched) {
210
238
  export class LivenessProbe {
211
239
  /**
212
240
  * @param {object} env environment adapter
213
- * @param {{staleAgeMs: number, now?: () => number, refreshMs?: number}} options
241
+ * @param {{staleAgeMs: number, inactiveMs?: number, now?: () => number,
242
+ * refreshMs?: number}} options
214
243
  */
215
- constructor(env, { staleAgeMs, now = () => Date.now(), refreshMs = 2000 }) {
244
+ constructor(
245
+ env,
246
+ {
247
+ staleAgeMs,
248
+ inactiveMs = DEFAULT_INACTIVE_MS,
249
+ now = () => Date.now(),
250
+ refreshMs = 2000,
251
+ }
252
+ ) {
216
253
  this.env = env;
217
254
  this.staleAgeMs = staleAgeMs;
255
+ this.inactiveMs = inactiveMs;
218
256
  this.now = now;
219
257
  this.refreshMs = refreshMs;
220
258
  this.refreshedAt = -Infinity;
@@ -225,6 +263,7 @@ export class LivenessProbe {
225
263
  this.pathOwners = new Map();
226
264
  this.pathStats = new Map();
227
265
  this.inodes = new Map();
266
+ this.agentSessions = new Map();
228
267
  // Bundle identifiers never change, so they outlive refreshes.
229
268
  this.bundleIds = new Map();
230
269
  }
@@ -247,6 +286,7 @@ export class LivenessProbe {
247
286
  this.pathStats.clear();
248
287
  this.pathOwners.clear();
249
288
  this.inodes.clear();
289
+ this.agentSessions.clear();
250
290
  }
251
291
 
252
292
  /** Reads the identifiers of the app bundles running processes come from. */
@@ -393,6 +433,7 @@ export class LivenessProbe {
393
433
  * @param {string[]} paths
394
434
  */
395
435
  async resolve(paths) {
436
+ await this.#resolveAgentSessions(paths);
396
437
  if (this.env.unreadableProcesses?.length) {
397
438
  const missing = [
398
439
  ...new Set(
@@ -436,6 +477,72 @@ export class LivenessProbe {
436
477
  }
437
478
  }
438
479
 
480
+ /**
481
+ * When each Claude Code session holding one of the paths was last active:
482
+ * its transcript, its session directory and the entries two levels below.
483
+ */
484
+ async #resolveAgentSessions(paths) {
485
+ const pending = new Map();
486
+ for (const target of paths) {
487
+ const session = agentSessionOf(target, this.env.path);
488
+ if (session && !this.agentSessions.has(session.dir)) {
489
+ pending.set(session.dir, session);
490
+ }
491
+ }
492
+ if (pending.size === 0) {
493
+ return;
494
+ }
495
+ this.homes ??= (await this.env.homeDirs?.().catch(() => null)) ?? [];
496
+ for (const session of pending.values()) {
497
+ const children = await this.env.list(session.dir);
498
+ const grandchildren = await Promise.all(
499
+ children
500
+ .filter((entry) => entry.type === 'dir')
501
+ .map((entry) => this.env.list(entry.path))
502
+ );
503
+ const candidates = [
504
+ session.dir,
505
+ ...children.map((entry) => entry.path),
506
+ ...grandchildren.flat().map((entry) => entry.path),
507
+ ...this.homes.map((home) =>
508
+ this.env.path.join(
509
+ home,
510
+ '.claude',
511
+ 'projects',
512
+ session.project,
513
+ `${session.id}.jsonl`
514
+ )
515
+ ),
516
+ ];
517
+ const stats = await Promise.all(
518
+ candidates.map((target) => this.env.stat?.(target))
519
+ );
520
+ const activeMs = Math.max(0, ...stats.map((stat) => stat?.mtimeMs ?? 0));
521
+ this.agentSessions.set(session.dir, { ...session, activeMs });
522
+ }
523
+ }
524
+
525
+ /** A path inside the scratchpad of a Claude Code session active recently. */
526
+ agentSessionUsage(paths) {
527
+ for (const target of paths) {
528
+ const session = this.agentSessions.get(
529
+ agentSessionOf(target, this.env.path)?.dir
530
+ );
531
+ if (!session) {
532
+ continue;
533
+ }
534
+ const age = this.now() - session.activeMs;
535
+ if (session.activeMs && age >= this.inactiveMs) {
536
+ continue;
537
+ }
538
+ const active = session.activeMs
539
+ ? `active ${formatDuration(Math.max(age, 0))} ago`
540
+ : 'activity unknown';
541
+ return `inside the scratchpad of Claude Code session ${session.id.slice(0, 8)} (${active}); kept until it is idle for ${formatDuration(this.inactiveMs)}`;
542
+ }
543
+ return null;
544
+ }
545
+
439
546
  recentWrite(newestMtimeMs) {
440
547
  if (!newestMtimeMs || this.staleAgeMs <= 0) {
441
548
  return null;
@@ -611,16 +718,17 @@ export class LivenessProbe {
611
718
  */
612
719
  busyReason(item) {
613
720
  const checks = item.checks ?? {};
614
- const paths = checks.perPath ? [] : (item.paths ?? []);
721
+ const all = item.paths ?? [];
615
722
  return (
616
- this.installationBlocker(item.paths ?? []) ??
723
+ this.installationBlocker(all) ??
617
724
  this.probeBlocker() ??
618
725
  (checks.mtime === false ? null : this.recentWrite(item.newestMtimeMs)) ??
726
+ this.agentSessionUsage(all) ??
619
727
  this.runningTool(checks.busy, checks.cwd, item) ??
620
- this.appUsage(item.paths ?? []) ??
621
- this.processUsage(paths) ??
728
+ this.appUsage(all) ??
729
+ this.processUsage(checks.perPath ? [] : all) ??
622
730
  this.executableUsage(checks.executables) ??
623
- this.uncertainUsage(item.paths ?? [])
731
+ this.uncertainUsage(all)
624
732
  );
625
733
  }
626
734