@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
@@ -26,6 +26,11 @@ function readRushToken() {
26
26
  if (!token) {
27
27
  throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
28
28
  }
29
+ const expiresAt = data.session?.expires_at;
30
+ if (typeof expiresAt === 'number' && expiresAt <= Date.now() / 1000) {
31
+ const expiredAt = new Date(expiresAt * 1000).toISOString();
32
+ throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
33
+ }
29
34
  return token;
30
35
  }
31
36
  async function api(method, endpoint, body) {
@@ -1,20 +1,14 @@
1
1
  /**
2
2
  * Reaper for orphaned / wedged keychain-helper processes.
3
3
  *
4
- * macOS keychain-helper calls are synchronous spawnSync invocations. When
5
- * coreauthd / LocalAuthentication hangs, the helper process (and sometimes its
6
- * parent `agents` process) can pile up forever. This module detects two classes
7
- * of stale process and kills them.
8
- *
9
- * The design splits cleanly into:
10
- * - a pure planner ({@link planKeychainReap}) that is unit-testable without a
11
- * real `ps` shell, and
12
- * - an impure driver ({@link reapOrphanedKeychainProcesses}) that shells `ps`
13
- * once per tick and executes kills through {@link killTree}.
4
+ * coreauthd / LocalAuthentication hangs can leave helper (and parent `agents`)
5
+ * processes pile up. The pure planner ({@link planKeychainReap}) is unit-testable
6
+ * without a real `ps`; the driver ({@link reapOrphanedKeychainProcesses}) shells
7
+ * `ps` once per tick and kills through {@link killTree}.
14
8
  */
15
- /** Elapsed grace before a helper whose parent exited is considered an orphan. */
9
+ /** Grace before a helper whose parent exited is considered an orphan. */
16
10
  export declare const ORPHAN_GRACE_SEC = 30;
17
- /** Elapsed grace before a helper with a live parent is considered stuck. */
11
+ /** Grace before a helper with a live parent is considered stuck. */
18
12
  export declare const STUCK_GRACE_SEC = 90;
19
13
  /** Snapshot of one process row from `ps`, fed into the pure planner. */
20
14
  export interface KeychainProcessSnapshot {
@@ -22,32 +16,19 @@ export interface KeychainProcessSnapshot {
22
16
  ppid: number;
23
17
  /** Elapsed seconds since the process started. */
24
18
  elapsedSec: number;
25
- /**
26
- * Stable start-time fingerprint from {@link captureProcessStartTime}.
27
- * `null` means "could not capture" — the planner must fail closed.
28
- */
19
+ /** Stable start-time fingerprint from {@link captureProcessStartTime}; null fails closed. */
29
20
  startTime: string | null;
30
- /**
31
- * True when this process is a REAP-ELIGIBLE helper invocation — the installed
32
- * helper binary running a short-lived keychain verb. False for a non-helper
33
- * process AND for the long-lived `watch-lock` watcher (see
34
- * {@link isReapableHelperCommand}).
35
- */
21
+ /** True for a reap-eligible helper invocation (short-lived keychain verb). */
36
22
  isHelper: boolean;
37
23
  /**
38
- * True when this process is the deliberately long-lived `watch-lock` watcher
39
- * (auto-lock-on-sleep). Mutually exclusive with {@link isHelper}:
40
- * {@link isReapableHelperCommand} keeps live-parent watch-locks out of the
41
- * stuck-helper path, and a SEPARATE orphan path in {@link planKeychainReap}
42
- * reaps them only once the owning daemon is provably dead (RUSH-2419).
24
+ * True for the deliberately long-lived `watch-lock` watcher
25
+ * (auto-lock-on-sleep). Mutually exclusive with {@link isHelper}; killing it
26
+ * silently disables auto-lock-on-sleep, so it is reaped only once the owning
27
+ * daemon is provably dead (RUSH-2419).
43
28
  */
44
29
  isWatchLock?: boolean;
45
30
  }
46
- /**
47
- * Tracked state for a stuck `agents` parent that has a live helper child.
48
- * Keyed by parent PID. The two-sweep debounce avoids acting on a racy `ps`
49
- * snapshot; the `stage` field drives child-first kill then parent escalation.
50
- */
31
+ /** Tracked state for a stuck parent across two-sweep debounce. */
51
32
  export interface StuckParentCandidate {
52
33
  pid: number;
53
34
  startTime: string | null;
@@ -58,7 +39,7 @@ export interface StuckParentCandidate {
58
39
  }
59
40
  /** Result of one planning pass. */
60
41
  export interface ReapPlan {
61
- /** PIDs that should be killed this tick. */
42
+ /** PIDs to kill this tick. */
62
43
  kill: number[];
63
44
  /** Candidates to carry forward to the next sweep. */
64
45
  nextCandidates: Map<number, StuckParentCandidate>;
@@ -66,51 +47,31 @@ export interface ReapPlan {
66
47
  /**
67
48
  * Pure predicate: decide which processes to kill given a `ps`-like snapshot.
68
49
  *
69
- * Mirrors the shape of {@link isExpiredPoolStray} in `lib/crabbox/lease.ts`:
70
- * a side-effect-free classifier that the impure driver shells `ps` for. Three
71
- * conservative reap classes:
50
+ * Three conservative classes:
51
+ * 1. Orphaned helper: PPID == 1, helper path, alive longer than grace.
52
+ * 2. Stuck `agents` parent: helper child alive longer than stuck grace,
53
+ * recorded on first sight, child killed on second sweep, parent on third.
54
+ * 3. Orphaned `watch-lock`: long-lived watcher whose owning daemon is gone.
72
55
  *
73
- * 1. Orphaned helper: PPID == 1, path-matches the helper, alive longer than
74
- * {@link ORPHAN_GRACE_SEC}.
75
- * 2. Stuck `agents` parent: helper child alive longer than
76
- * {@link STUCK_GRACE_SEC}. Recorded on first sight, child killed on the
77
- * second consecutive sweep with the same PID + startTime, parent killed on
78
- * the third sweep if the helper child is still present.
79
- * 3. Orphaned `watch-lock` watcher (RUSH-2419): the long-lived auto-lock
80
- * child whose owning daemon is provably dead. {@link isReapableHelperCommand}
81
- * still excludes live-parent watch-locks from class 1/2; this path only
82
- * reaps when the parent is gone from the snapshot (Unix reparents to init
83
- * after daemon death) and the watcher itself has a start-time fingerprint.
84
- *
85
- * Never reaps a process whose start time could not be captured, whose path does
86
- * not match the helper, or (for stuck parents) whose parent is no longer in the
87
- * snapshot. A watch-lock whose parent IS still in the snapshot is never touched.
56
+ * Fails closed when start time can't be captured.
88
57
  */
89
58
  export declare function planKeychainReap(snapshots: KeychainProcessSnapshot[], now: number, prevCandidates: ReadonlyMap<number, StuckParentCandidate>): ReapPlan;
90
59
  /**
91
60
  * Parse macOS `ps -o etime=` elapsed time into whole seconds.
92
61
  *
93
- * BSD `etime` renders as `[[dd-]hh:]mm:ss` (e.g. `05:03`, `01:02:03`,
94
- * `14-04:10:52`). This is the portable keyword: `etimes` (raw seconds) is a
95
- * GNU/Linux procps extension that macOS `ps` rejects with a non-zero exit, so
96
- * the reaper — which only ever runs on darwin — must read `etime`.
97
- * Returns null for an unparseable value so the caller drops the row.
62
+ * BSD `etime` renders as `[[dd-]hh:]mm:ss`. `etimes` (raw seconds) is a GNU
63
+ * extension that macOS `ps` rejects, so darwin uses `etime`.
98
64
  */
99
65
  export declare function parseEtimeToSeconds(raw: string): number | null;
100
66
  /**
101
- * Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
102
- * helper binary running a short-lived keychain verb (get/has/list/set/delete/
103
- * migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
104
- * command AND for the deliberately long-lived `watch-lock` watcher — matching by
105
- * the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
106
- * mistaken for a stuck read and killed. Pure; unit-tested.
67
+ * Whether a `ps` command line is a reap-eligible helper invocation: the installed
68
+ * helper binary running a short-lived keychain verb. Returns false for the
69
+ * long-lived `watch-lock` watcher, which must never be mistaken for a stuck read.
107
70
  */
108
71
  export declare function isReapableHelperCommand(command: string, helperPath: string): boolean;
109
72
  /**
110
73
  * Whether a `ps` command line is the deliberately long-lived `watch-lock`
111
- * watcher (auto-lock-on-sleep). Inverse of {@link isReapableHelperCommand} for
112
- * the watch-lock verb only — used by the orphaned-watch-lock reaper path
113
- * (RUSH-2419). Pure; unit-tested.
74
+ * watcher. Inverse of {@link isReapableHelperCommand} for the watch-lock verb.
114
75
  */
115
76
  export declare function isWatchLockHelperCommand(command: string, helperPath: string): boolean;
116
77
  /** Test seam: reset the persisted candidate state. */
@@ -119,11 +80,8 @@ export declare function resetKeychainReaperCandidatesForTest(): void;
119
80
  * Impure driver: snapshot all processes once with `ps`, plan the reap, then
120
81
  * execute kills through {@link killTree}.
121
82
  *
122
- * Path-matching uses the full helper path (proc_pidpath-style), not just the
123
- * executable name, so an unrelated binary named "Agents CLI" is never targeted.
124
- *
125
- * Returns on non-darwin platforms without shelling anything — the helper only
126
- * exists on macOS.
83
+ * Path-matches the full helper path so an unrelated binary named "Agents CLI" is
84
+ * never targeted. Returns on non-darwin without shelling anything.
127
85
  */
128
86
  export declare function reapOrphanedKeychainProcesses(): {
129
87
  reaped: number;
@@ -1,46 +1,28 @@
1
1
  /**
2
2
  * Reaper for orphaned / wedged keychain-helper processes.
3
3
  *
4
- * macOS keychain-helper calls are synchronous spawnSync invocations. When
5
- * coreauthd / LocalAuthentication hangs, the helper process (and sometimes its
6
- * parent `agents` process) can pile up forever. This module detects two classes
7
- * of stale process and kills them.
8
- *
9
- * The design splits cleanly into:
10
- * - a pure planner ({@link planKeychainReap}) that is unit-testable without a
11
- * real `ps` shell, and
12
- * - an impure driver ({@link reapOrphanedKeychainProcesses}) that shells `ps`
13
- * once per tick and executes kills through {@link killTree}.
4
+ * coreauthd / LocalAuthentication hangs can leave helper (and parent `agents`)
5
+ * processes pile up. The pure planner ({@link planKeychainReap}) is unit-testable
6
+ * without a real `ps`; the driver ({@link reapOrphanedKeychainProcesses}) shells
7
+ * `ps` once per tick and kills through {@link killTree}.
14
8
  */
15
9
  import { execFileSync } from 'child_process';
16
10
  import { killTree, captureProcessStartTime } from '../platform/process.js';
17
11
  import { getKeychainHelperPath } from './install-helper.js';
18
- /** Elapsed grace before a helper whose parent exited is considered an orphan. */
12
+ /** Grace before a helper whose parent exited is considered an orphan. */
19
13
  export const ORPHAN_GRACE_SEC = 30;
20
- /** Elapsed grace before a helper with a live parent is considered stuck. */
14
+ /** Grace before a helper with a live parent is considered stuck. */
21
15
  export const STUCK_GRACE_SEC = 90;
22
16
  /**
23
17
  * Pure predicate: decide which processes to kill given a `ps`-like snapshot.
24
18
  *
25
- * Mirrors the shape of {@link isExpiredPoolStray} in `lib/crabbox/lease.ts`:
26
- * a side-effect-free classifier that the impure driver shells `ps` for. Three
27
- * conservative reap classes:
28
- *
29
- * 1. Orphaned helper: PPID == 1, path-matches the helper, alive longer than
30
- * {@link ORPHAN_GRACE_SEC}.
31
- * 2. Stuck `agents` parent: helper child alive longer than
32
- * {@link STUCK_GRACE_SEC}. Recorded on first sight, child killed on the
33
- * second consecutive sweep with the same PID + startTime, parent killed on
34
- * the third sweep if the helper child is still present.
35
- * 3. Orphaned `watch-lock` watcher (RUSH-2419): the long-lived auto-lock
36
- * child whose owning daemon is provably dead. {@link isReapableHelperCommand}
37
- * still excludes live-parent watch-locks from class 1/2; this path only
38
- * reaps when the parent is gone from the snapshot (Unix reparents to init
39
- * after daemon death) and the watcher itself has a start-time fingerprint.
19
+ * Three conservative classes:
20
+ * 1. Orphaned helper: PPID == 1, helper path, alive longer than grace.
21
+ * 2. Stuck `agents` parent: helper child alive longer than stuck grace,
22
+ * recorded on first sight, child killed on second sweep, parent on third.
23
+ * 3. Orphaned `watch-lock`: long-lived watcher whose owning daemon is gone.
40
24
  *
41
- * Never reaps a process whose start time could not be captured, whose path does
42
- * not match the helper, or (for stuck parents) whose parent is no longer in the
43
- * snapshot. A watch-lock whose parent IS still in the snapshot is never touched.
25
+ * Fails closed when start time can't be captured.
44
26
  */
45
27
  export function planKeychainReap(snapshots, now, prevCandidates) {
46
28
  const pidMap = new Map(snapshots.map((s) => [s.pid, s]));
@@ -50,8 +32,7 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
50
32
  if (!s.isHelper)
51
33
  continue;
52
34
  if (s.ppid === 1) {
53
- // Orphaned helper: parent is init/launchd. Fail closed if we can't prove
54
- // the process identity with a start-time fingerprint.
35
+ // Orphaned helper: fail closed without a start-time fingerprint.
55
36
  if (s.elapsedSec <= ORPHAN_GRACE_SEC)
56
37
  continue;
57
38
  if (s.startTime == null)
@@ -59,8 +40,7 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
59
40
  kill.push(s.pid);
60
41
  continue;
61
42
  }
62
- // Stuck agents: a helper with a live parent that has been wedged for longer
63
- // than the interactive timeout. The parent must still exist in the snapshot.
43
+ // Stuck agents: helper with a live parent wedged longer than the timeout.
64
44
  if (s.elapsedSec <= STUCK_GRACE_SEC)
65
45
  continue;
66
46
  const parent = pidMap.get(s.ppid);
@@ -74,13 +54,12 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
74
54
  prev.helperStartTime === s.startTime &&
75
55
  prev.startTime === parent.startTime) {
76
56
  if (prev.stage === 'watch') {
77
- // Second sweep: kill the child first so the parent's spawnSync returns.
78
- // Keep the parent candidate at stage 'escalate' for the next sweep.
57
+ // Second sweep: kill the child first so spawnSync returns; escalate next.
79
58
  kill.push(s.pid);
80
59
  nextCandidates.set(parent.pid, { ...prev, stage: 'escalate' });
81
60
  continue;
82
61
  }
83
- // Third sweep: child did not free the parent, so the parent itself is wedged.
62
+ // Third sweep: child did not free the parent, so the parent is wedged.
84
63
  kill.push(parent.pid);
85
64
  continue;
86
65
  }
@@ -94,32 +73,21 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
94
73
  stage: 'watch',
95
74
  });
96
75
  }
97
- // Separate path for orphaned watch-lock watchers (RUSH-2419). Does NOT
98
- // weaken {@link isReapableHelperCommand}: a watch-lock with a live parent
99
- // stays isHelper=false and is skipped above. Here we only kill when the
100
- // owning daemon is provably absent from the process table.
76
+ // Separate path for orphaned watch-lock watchers: only reap when the owning
77
+ // daemon is provably absent from the process table.
101
78
  for (const s of snapshots) {
102
79
  if (!s.isWatchLock)
103
80
  continue;
104
- // Fail closed: no start-time fingerprint → refuse to kill (pid-reuse guard
105
- // for the process we are about to target, same as orphan helpers).
106
81
  if (s.startTime == null)
107
82
  continue;
108
83
  if (s.elapsedSec <= ORPHAN_GRACE_SEC)
109
84
  continue;
110
- // Live parent in this snapshot → owning daemon is still up. Leave it alone
111
- // (this is the property isReapableHelperCommand protects for stuck-helper
112
- // reaping; re-assert it here so a mis-tagged row cannot be killed either).
113
85
  if (s.ppid !== 1) {
114
86
  const parent = pidMap.get(s.ppid);
115
87
  if (parent)
116
88
  continue;
117
- // Parent pid not listed: the process occupying that slot is gone. On Unix
118
- // a dead parent reparents the child to init; a missing parent with a
119
- // non-1 ppid is the race window before reparenting. Either way the owner
120
- // is dead — safe to reap after grace + fingerprint.
89
+ // Parent missing means the owner is dead (or reparenting race); safe to reap.
121
90
  }
122
- // ppid===1 (reparented to init/launchd) or parent missing → orphaned.
123
91
  kill.push(s.pid);
124
92
  }
125
93
  return { kill, nextCandidates };
@@ -127,11 +95,8 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
127
95
  /**
128
96
  * Parse macOS `ps -o etime=` elapsed time into whole seconds.
129
97
  *
130
- * BSD `etime` renders as `[[dd-]hh:]mm:ss` (e.g. `05:03`, `01:02:03`,
131
- * `14-04:10:52`). This is the portable keyword: `etimes` (raw seconds) is a
132
- * GNU/Linux procps extension that macOS `ps` rejects with a non-zero exit, so
133
- * the reaper — which only ever runs on darwin — must read `etime`.
134
- * Returns null for an unparseable value so the caller drops the row.
98
+ * BSD `etime` renders as `[[dd-]hh:]mm:ss`. `etimes` (raw seconds) is a GNU
99
+ * extension that macOS `ps` rejects, so darwin uses `etime`.
135
100
  */
136
101
  export function parseEtimeToSeconds(raw) {
137
102
  const m = raw.match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
@@ -150,8 +115,6 @@ export function parseEtimeToSeconds(raw) {
150
115
  *
151
116
  * Expected format from `ps -ax -o pid=,ppid=,etime=,command=`:
152
117
  * "<pid> <ppid> <etime> <command...>"
153
- * where `<etime>` is BSD elapsed time (`[[dd-]hh:]mm:ss`). The command field is
154
- * the remainder of the line and may contain spaces.
155
118
  */
156
119
  function parsePsLine(line) {
157
120
  const trimmed = line.trim();
@@ -168,22 +131,12 @@ function parsePsLine(line) {
168
131
  return null;
169
132
  return { pid, ppid, elapsedSec, command };
170
133
  }
171
- /**
172
- * The one helper verb that is DELIBERATELY long-lived: the broker's auto-lock
173
- * sleep/lock watcher (`spawn(getKeychainHelperPath(), ['watch-lock'], …)` in
174
- * `agent.ts`). It lives for the broker's whole hold — potentially days — as a
175
- * healthy child of the live broker/daemon, emitting LOCK/SLEEP lines that wipe
176
- * the in-memory secret store on sleep. It is NOT a stuck keychain read, so the
177
- * reaper must never target it: killing it silently disables auto-lock-on-sleep.
178
- */
134
+ /** The one helper verb that is deliberately long-lived: the auto-lock watcher. */
179
135
  const HELPER_WATCH_LOCK_VERB = 'watch-lock';
180
136
  /**
181
- * Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
182
- * helper binary running a short-lived keychain verb (get/has/list/set/delete/
183
- * migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
184
- * command AND for the deliberately long-lived `watch-lock` watcher — matching by
185
- * the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
186
- * mistaken for a stuck read and killed. Pure; unit-tested.
137
+ * Whether a `ps` command line is a reap-eligible helper invocation: the installed
138
+ * helper binary running a short-lived keychain verb. Returns false for the
139
+ * long-lived `watch-lock` watcher, which must never be mistaken for a stuck read.
187
140
  */
188
141
  export function isReapableHelperCommand(command, helperPath) {
189
142
  if (command === helperPath)
@@ -195,9 +148,7 @@ export function isReapableHelperCommand(command, helperPath) {
195
148
  }
196
149
  /**
197
150
  * Whether a `ps` command line is the deliberately long-lived `watch-lock`
198
- * watcher (auto-lock-on-sleep). Inverse of {@link isReapableHelperCommand} for
199
- * the watch-lock verb only — used by the orphaned-watch-lock reaper path
200
- * (RUSH-2419). Pure; unit-tested.
151
+ * watcher. Inverse of {@link isReapableHelperCommand} for the watch-lock verb.
201
152
  */
202
153
  export function isWatchLockHelperCommand(command, helperPath) {
203
154
  if (!command.startsWith(`${helperPath} `))
@@ -215,11 +166,8 @@ export function resetKeychainReaperCandidatesForTest() {
215
166
  * Impure driver: snapshot all processes once with `ps`, plan the reap, then
216
167
  * execute kills through {@link killTree}.
217
168
  *
218
- * Path-matching uses the full helper path (proc_pidpath-style), not just the
219
- * executable name, so an unrelated binary named "Agents CLI" is never targeted.
220
- *
221
- * Returns on non-darwin platforms without shelling anything — the helper only
222
- * exists on macOS.
169
+ * Path-matches the full helper path so an unrelated binary named "Agents CLI" is
170
+ * never targeted. Returns on non-darwin without shelling anything.
223
171
  */
224
172
  export function reapOrphanedKeychainProcesses() {
225
173
  const details = [];
@@ -249,11 +197,8 @@ export function reapOrphanedKeychainProcesses() {
249
197
  if (!parsed)
250
198
  continue;
251
199
  const { pid, ppid, elapsedSec, command } = parsed;
252
- // Reap-eligible = the helper binary running a short-lived keychain verb. The
253
- // full-argv match excludes the deliberately long-lived `watch-lock` watcher,
254
- // whose live-parent child would otherwise be killed as if it were stuck
255
- // (RUSH-2232 — that silently disabled auto-lock-on-sleep). Orphaned
256
- // watch-locks are tracked separately via isWatchLock (RUSH-2419).
200
+ // Match the full argv so the long-lived `watch-lock` watcher is never
201
+ // mistaken for a stuck read. Orphaned watch-locks are tracked separately.
257
202
  const isHelper = isReapableHelperCommand(command, helperPath);
258
203
  const isWatchLock = isWatchLockHelperCommand(command, helperPath);
259
204
  rows.push({ pid, ppid, elapsedSec, isHelper, isWatchLock, startTime: null });