@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.
- package/CHANGELOG.md +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
+
/** Grace before a helper whose parent exited is considered an orphan. */
|
|
16
10
|
export declare const ORPHAN_GRACE_SEC = 30;
|
|
17
|
-
/**
|
|
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
|
|
39
|
-
* (auto-lock-on-sleep). Mutually exclusive with {@link isHelper}
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
*
|
|
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` (
|
|
94
|
-
*
|
|
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
|
|
102
|
-
* helper binary running a short-lived keychain verb
|
|
103
|
-
*
|
|
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
|
|
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-
|
|
123
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
12
|
+
/** Grace before a helper whose parent exited is considered an orphan. */
|
|
19
13
|
export const ORPHAN_GRACE_SEC = 30;
|
|
20
|
-
/**
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
98
|
-
//
|
|
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
|
|
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` (
|
|
131
|
-
*
|
|
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
|
|
182
|
-
* helper binary running a short-lived keychain verb
|
|
183
|
-
*
|
|
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
|
|
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-
|
|
219
|
-
*
|
|
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
|
-
//
|
|
253
|
-
//
|
|
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 });
|