@phnx-labs/agents-cli 1.22.107 → 1.22.110
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 +96 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/browser-sessions-picker.js +2 -1
- package/dist/commands/computer-sessions-picker.js +2 -1
- package/dist/commands/cost.js +2 -1
- package/dist/commands/daemon.js +45 -40
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/exec.js +4 -2
- package/dist/commands/fork.js +6 -4
- package/dist/commands/logs.js +2 -1
- package/dist/commands/monitors.js +15 -2
- package/dist/commands/routines.js +46 -6
- package/dist/commands/sessions-backfill.d.ts +30 -0
- package/dist/commands/sessions-backfill.js +72 -0
- package/dist/commands/sessions-backup-setup.d.ts +12 -0
- package/dist/commands/sessions-backup-setup.js +65 -0
- package/dist/commands/sessions-inject.d.ts +3 -3
- package/dist/commands/sessions-inject.js +5 -11
- package/dist/commands/sessions-picker.js +10 -7
- package/dist/commands/sessions-resume.d.ts +17 -2
- package/dist/commands/sessions-resume.js +88 -12
- package/dist/commands/sessions.js +46 -24
- package/dist/commands/share.js +15 -2
- package/dist/commands/sync.js +30 -1
- package/dist/commands/utils.d.ts +9 -0
- package/dist/commands/utils.js +18 -0
- package/dist/commands/watchdog.js +4 -1
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +17 -1
- package/dist/lib/accounting/usage-sync.d.ts +22 -0
- package/dist/lib/accounting/usage-sync.js +70 -6
- package/dist/lib/accounting/usage.d.ts +32 -0
- package/dist/lib/accounting/usage.js +37 -2
- package/dist/lib/accounts/slots.js +7 -0
- package/dist/lib/agent-spec/agents.d.ts +60 -8
- package/dist/lib/agent-spec/agents.js +118 -45
- package/dist/lib/auth-health.js +19 -0
- package/dist/lib/claude-statusline.js +5 -0
- package/dist/lib/computer/sessions-list.js +2 -1
- package/dist/lib/daemon/auth-sync-service.d.ts +2 -0
- package/dist/lib/daemon/auth-sync-service.js +2 -2
- package/dist/lib/daemon/daemon.d.ts +34 -1
- package/dist/lib/daemon/daemon.js +70 -9
- package/dist/lib/daemon/leaked-daemons.d.ts +60 -0
- package/dist/lib/daemon/leaked-daemons.js +180 -0
- package/dist/lib/daemon/session-title-service.d.ts +41 -0
- package/dist/lib/daemon/session-title-service.js +76 -0
- package/dist/lib/daemon/usage-sync-service.d.ts +9 -1
- package/dist/lib/daemon/usage-sync-service.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.js +6 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +32 -1
- package/dist/lib/fleet-shared-state.d.ts +2 -0
- package/dist/lib/hooks/install.js +78 -64
- package/dist/lib/mailbox-target.js +2 -1
- package/dist/lib/models.js +76 -5
- package/dist/lib/session/active.d.ts +93 -16
- package/dist/lib/session/active.js +80 -14
- package/dist/lib/session/cloud.js +3 -1
- package/dist/lib/session/db.d.ts +44 -1
- package/dist/lib/session/db.js +122 -17
- package/dist/lib/session/fork.d.ts +10 -2
- package/dist/lib/session/fork.js +11 -2
- package/dist/lib/session/live-metadata.js +6 -0
- package/dist/lib/session/mirror.d.ts +3 -2
- package/dist/lib/session/mirror.js +5 -2
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +136 -2
- package/dist/lib/session/recovery.d.ts +9 -1
- package/dist/lib/session/recovery.js +14 -4
- package/dist/lib/session/remote/watch.js +18 -8
- package/dist/lib/session/title.d.ts +256 -0
- package/dist/lib/session/title.js +312 -0
- package/dist/lib/session/tool-calls.d.ts +1 -1
- package/dist/lib/session/tool-calls.js +40 -5
- package/dist/lib/session/tool-index.d.ts +5 -0
- package/dist/lib/session/tool-index.js +1 -0
- package/dist/lib/session/types.d.ts +11 -0
- package/dist/lib/share/backend.d.ts +23 -0
- package/dist/lib/share/backend.js +24 -0
- package/dist/lib/share/provision.d.ts +12 -0
- package/dist/lib/share/provision.js +30 -0
- package/dist/lib/share/worker-template.js +273 -0
- package/dist/lib/startup/root-command.d.ts +2 -0
- package/dist/lib/startup/root-command.js +22 -0
- package/dist/lib/terminal/engine.js +13 -1
- package/dist/lib/traces/sync.js +1 -0
- package/dist/lib/usage-refresh.d.ts +58 -7
- package/dist/lib/usage-refresh.js +145 -23
- package/dist/lib/watchdog/runner.d.ts +3 -0
- package/dist/lib/watchdog/runner.js +1 -0
- package/dist/session-tracker/dist/install-hook.js +4 -4
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, re
|
|
|
32
32
|
import { ServiceSupervisor } from './supervisor.js';
|
|
33
33
|
import { SessionIndexService } from './session-index-service.js';
|
|
34
34
|
import { SessionSummarizerService } from './session-summarizer-service.js';
|
|
35
|
+
import { SessionTitleService } from './session-title-service.js';
|
|
35
36
|
import { MonitorEngineService } from './monitor-engine-service.js';
|
|
36
37
|
import { AccountUsageService, AccountAuthService } from './account-state-daemon-service.js';
|
|
37
38
|
import { CatchupService } from './catchup-service.js';
|
|
@@ -77,6 +78,16 @@ const LOG_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
|
77
78
|
const LOG_ROTATE_COUNT = 3;
|
|
78
79
|
const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
79
80
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
81
|
+
/**
|
|
82
|
+
* The service-manager identifiers of the REAL install's daemon — never
|
|
83
|
+
* namespaced. A caller under a redirected HOME (a test/e2e harness) uses these
|
|
84
|
+
* to recognize the box's production daemon as owned, where
|
|
85
|
+
* `daemonSystemdUnitName()`/`daemonServiceLabel()` would name only its own
|
|
86
|
+
* sandbox job (W4, PHNX-3736).
|
|
87
|
+
*/
|
|
88
|
+
export function productionDaemonServiceNames() {
|
|
89
|
+
return { systemdUnit: SYSTEMD_UNIT, launchdLabel: PLIST_NAME };
|
|
90
|
+
}
|
|
80
91
|
/**
|
|
81
92
|
* RUSH-2639 (residual): launchd/systemd route `unload`/`load`/`list` by the
|
|
82
93
|
* service identifier ALONE, never by the plist/unit file's path. Baking the
|
|
@@ -286,8 +297,8 @@ function getSystemdUnitPath() {
|
|
|
286
297
|
return path.join(os.homedir(), '.config', 'systemd', 'user', daemonSystemdUnitName());
|
|
287
298
|
}
|
|
288
299
|
/** Read the stored daemon PID from disk. Returns null if not present or invalid. */
|
|
289
|
-
export function readDaemonPid() {
|
|
290
|
-
const pidPath = getPidPath();
|
|
300
|
+
export function readDaemonPid(daemonDir) {
|
|
301
|
+
const pidPath = daemonDir ? path.join(daemonDir, PID_FILE) : getPidPath();
|
|
291
302
|
if (!fs.existsSync(pidPath))
|
|
292
303
|
return null;
|
|
293
304
|
try {
|
|
@@ -1060,6 +1071,12 @@ export async function runDaemon() {
|
|
|
1060
1071
|
supervisor.register(new AttentionNotifyService());
|
|
1061
1072
|
else
|
|
1062
1073
|
log('INFO', 'Attention-notify service disabled');
|
|
1074
|
+
// Session titles (PHNX-3797) — generates each session row's headline once,
|
|
1075
|
+
// with a cheap model, off the request path.
|
|
1076
|
+
if (isEnabled('session-title'))
|
|
1077
|
+
supervisor.register(new SessionTitleService());
|
|
1078
|
+
else
|
|
1079
|
+
log('INFO', 'Session-title service disabled');
|
|
1063
1080
|
// Watchdog, device-probe, and self-heal are all periodic services managed
|
|
1064
1081
|
// by the ServiceSupervisor (RUSH-3193 P3). Each is gated the same way as
|
|
1065
1082
|
// the socket services above; state-dir-check is registered separately,
|
|
@@ -1655,19 +1672,27 @@ export { getAgentsBinPath };
|
|
|
1655
1672
|
* the daemon hasn't yet written its pid file but launchd/systemd already report
|
|
1656
1673
|
* it running — so a start never has to surface a null PID for a daemon that is
|
|
1657
1674
|
* in fact up. Returns null when the service isn't running or the query fails.
|
|
1675
|
+
*
|
|
1676
|
+
* `names` defaults to THIS process's (possibly sandbox-namespaced) job; a
|
|
1677
|
+
* caller under a redirected HOME passes `productionDaemonServiceNames()` to
|
|
1678
|
+
* ask after the real install's unit instead. Read-only either way.
|
|
1658
1679
|
*/
|
|
1659
|
-
function readServiceManagerPid(platform = os.platform()) {
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1680
|
+
export function readServiceManagerPid(platform = os.platform(), names) {
|
|
1681
|
+
const resolved = names ?? { systemdUnit: daemonSystemdUnitName(), launchdLabel: daemonServiceLabel() };
|
|
1682
|
+
// The registration gate exists because a sandboxed process must not REGISTER
|
|
1683
|
+
// or tear down jobs in the real per-user service manager. Asking after an
|
|
1684
|
+
// explicitly named job is read-only — in particular the production unit from
|
|
1685
|
+
// a redirected-HOME caller (W4) — and mutates nothing, so it is let through.
|
|
1686
|
+
if (names === undefined && !serviceManagerRegistrationAllowed().allowed)
|
|
1662
1687
|
return null;
|
|
1663
1688
|
try {
|
|
1664
1689
|
if (platform === 'linux') {
|
|
1665
|
-
const out = execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value',
|
|
1690
|
+
const out = execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', resolved.systemdUnit], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1666
1691
|
const pid = parseInt(out, 10);
|
|
1667
1692
|
return !isNaN(pid) && pid > 0 ? pid : null;
|
|
1668
1693
|
}
|
|
1669
1694
|
if (platform === 'darwin') {
|
|
1670
|
-
const out = execFileSync('launchctl', ['list',
|
|
1695
|
+
const out = execFileSync('launchctl', ['list', resolved.launchdLabel], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1671
1696
|
const m = out.match(/"PID"\s*=\s*(\d+)/);
|
|
1672
1697
|
if (m) {
|
|
1673
1698
|
const pid = parseInt(m[1], 10);
|
|
@@ -1678,12 +1703,47 @@ function readServiceManagerPid(platform = os.platform()) {
|
|
|
1678
1703
|
catch { /* not running / manager unavailable */ }
|
|
1679
1704
|
return null;
|
|
1680
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Thrown when a daemon LAUNCH is attempted under a redirected HOME without the
|
|
1708
|
+
* explicit test opt-in (W4, PHNX-3736). `bootstrap.ts` prints the message
|
|
1709
|
+
* without a stack — this is user-actionable, not an engineering bug.
|
|
1710
|
+
*/
|
|
1711
|
+
export class RedirectedHomeDaemonError extends Error {
|
|
1712
|
+
name = 'RedirectedHomeDaemonError';
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* W4 (PHNX-3736): never LAUNCH a daemon under a redirected (sandbox/test) HOME
|
|
1716
|
+
* without an explicit opt-in. A daemon started there keeps its own pid file
|
|
1717
|
+
* under the temp home, so the real install's pid-file takeover can never see
|
|
1718
|
+
* it — the leaked `HOME=/tmp/pin-e2e-<pid>` daemon that ran 4+ days on
|
|
1719
|
+
* yosemite-s1 was launched exactly this way by a headless e2e session that
|
|
1720
|
+
* never cleaned up. RUSH-3021 closed this for `ensureDaemonStarted`'s
|
|
1721
|
+
* AUTO-start path but left the explicit `startDaemon()` open, which is the
|
|
1722
|
+
* path the e2e harness took.
|
|
1723
|
+
*
|
|
1724
|
+
* Placed after the `already-running` early-return: reporting a live daemon (so
|
|
1725
|
+
* `daemon stop` can still kill a leaked one) must stay possible under any
|
|
1726
|
+
* HOME. `AGENTS_ALLOW_TEST_DAEMON=1` is the deliberate test/e2e seam — a
|
|
1727
|
+
* harness that sets it owns stopping what it starts.
|
|
1728
|
+
*/
|
|
1729
|
+
function assertDaemonLaunchHomeAllowed() {
|
|
1730
|
+
const suffix = isolatedHomeSuffix();
|
|
1731
|
+
if (!suffix)
|
|
1732
|
+
return;
|
|
1733
|
+
if (process.env.AGENTS_ALLOW_TEST_DAEMON === '1')
|
|
1734
|
+
return;
|
|
1735
|
+
throw new RedirectedHomeDaemonError(`refusing to start the daemon under a redirected HOME (sandbox-${suffix}): ` +
|
|
1736
|
+
`a daemon launched here keeps its own pid file under ${process.env.HOME}, invisible to the real ` +
|
|
1737
|
+
`install's pid-file takeover, and outlives whatever launched it (PHNX-3736). ` +
|
|
1738
|
+
`For a deliberate test/e2e launch set AGENTS_ALLOW_TEST_DAEMON=1 — and stop the daemon when done.`);
|
|
1739
|
+
}
|
|
1681
1740
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
1682
1741
|
export function startDaemon(agentsBin) {
|
|
1683
1742
|
if (isDaemonRunning()) {
|
|
1684
1743
|
const pid = readDaemonPid();
|
|
1685
1744
|
return { pid, method: 'already-running' };
|
|
1686
1745
|
}
|
|
1746
|
+
assertDaemonLaunchHomeAllowed();
|
|
1687
1747
|
const releaseLock = acquireStartLock();
|
|
1688
1748
|
if (!releaseLock) {
|
|
1689
1749
|
// Another process is already starting the daemon
|
|
@@ -1769,8 +1829,9 @@ export function ensureDaemonStarted() {
|
|
|
1769
1829
|
// recursive teardown rm (ENOTEMPTY). Placed after the already-running branch
|
|
1770
1830
|
// — reporting a live daemon stays allowed, same as the circuit breaker.
|
|
1771
1831
|
// AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1 is the test seam for suites
|
|
1772
|
-
// that exercise daemon startup deliberately
|
|
1773
|
-
//
|
|
1832
|
+
// that exercise daemon startup deliberately. The explicit `agents daemon
|
|
1833
|
+
// start` path is gated separately by startDaemon's own redirected-HOME
|
|
1834
|
+
// refusal (AGENTS_ALLOW_TEST_DAEMON=1, W4/PHNX-3736).
|
|
1774
1835
|
if (!serviceManagerRegistrationAllowed().allowed)
|
|
1775
1836
|
return null;
|
|
1776
1837
|
// RUSH-2418: the auto-start circuit breaker. A daemon that dies during
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon state dir for an ARBITRARY home — the same layout state.ts's
|
|
3
|
+
* DAEMON_DIR chain produces (`<home>/.agents/.cache/helpers/daemon`). A caller
|
|
4
|
+
* under a redirected HOME (a test/e2e harness) uses this to find the REAL
|
|
5
|
+
* install's daemon records. Kept here rather than exported from state.ts: any
|
|
6
|
+
* edit to state.ts selects its entire static-import closure into the required
|
|
7
|
+
* impact gate (186 files, 295s against the 240s budget, measured on this
|
|
8
|
+
* change's first CI run), so the helper cannot live there without a
|
|
9
|
+
* budget-policy change. If state.ts's layout ever moves, this must move with
|
|
10
|
+
* it — the two are the same address.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getDaemonDirForHome(home: string): string;
|
|
13
|
+
/** One live `__daemon-run` process from the box-wide `ps` scan. */
|
|
14
|
+
export interface DaemonRunProcess {
|
|
15
|
+
pid: number;
|
|
16
|
+
/** Owning uid from `ps`, or null when unavailable. */
|
|
17
|
+
uid: number | null;
|
|
18
|
+
/** Whitespace-tokenized argv (`ps` renders it unquoted). */
|
|
19
|
+
tokens: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every live `__daemon-run` process on this box, regardless of which install
|
|
23
|
+
* launched it or which state dir it serves. POSIX-only (uses `ps`); a no-op on
|
|
24
|
+
* Windows.
|
|
25
|
+
*
|
|
26
|
+
* `getDaemonLaunch` always spawns `<node> <entry> __daemon-run` with nothing
|
|
27
|
+
* after it — the ONLY argv `__daemon-run` ever appears in for a real daemon.
|
|
28
|
+
* A substring/regex test anywhere in the full command line is not enough: an
|
|
29
|
+
* `agents run claude "<prompt>"` invocation whose prompt happens to quote the
|
|
30
|
+
* literal text `__daemon-run` (this ticket's own brief does) matches that test
|
|
31
|
+
* too, and was observed producing false "duplicate daemon" rows. Requiring it
|
|
32
|
+
* to be the LAST whitespace-delimited token is the actual invariant.
|
|
33
|
+
*
|
|
34
|
+
* This raw box-wide scan is deliberately NOT the duplicate-detection scope
|
|
35
|
+
* (RUSH-2368): a `__daemon-run` under a different HOME serves a different
|
|
36
|
+
* `getDaemonDir()` and is not a duplicate of THIS device's daemon, however
|
|
37
|
+
* `ps` sees it — a leaked vitest fixture under its own `/tmp` HOME matched
|
|
38
|
+
* this scan and was reported as a stray to `kill`. Callers attach their own
|
|
39
|
+
* scope: `agents daemon status` gates on the instance registry,
|
|
40
|
+
* {@link findLeakedDaemons} on the owner records above.
|
|
41
|
+
*/
|
|
42
|
+
export declare function listDaemonRunProcesses(): DaemonRunProcess[];
|
|
43
|
+
/** A `__daemon-run` process no owner record names. */
|
|
44
|
+
export interface LeakedDaemon {
|
|
45
|
+
pid: number;
|
|
46
|
+
/** The HOME the process runs under, or null when it cannot be read. */
|
|
47
|
+
home: string | null;
|
|
48
|
+
/** The process's start time as `ps lstart` renders it, or null when unavailable. */
|
|
49
|
+
startedAt: string | null;
|
|
50
|
+
/** The launch entry (the argv token before `__daemon-run`), best-effort. */
|
|
51
|
+
entry: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every `__daemon-run` process running as THIS uid that no owner record names:
|
|
55
|
+
* neither the service manager's unit main PID nor the recorded
|
|
56
|
+
* `<daemonDir>/daemon.pid`. Each result carries the process's HOME and start
|
|
57
|
+
* time so the report can show them. An empty list means no leak — including
|
|
58
|
+
* the common stopped-daemon case, where there is simply nothing running.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findLeakedDaemons(): LeakedDaemon[];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaked-daemon detection (W4, PHNX-3736).
|
|
3
|
+
*
|
|
4
|
+
* One daemon per device is the contract: the always-on process behind
|
|
5
|
+
* `agents __daemon-run` is either the service manager's unit main PID or the
|
|
6
|
+
* pid recorded in `<daemonDir>/daemon.pid`. Anything else running
|
|
7
|
+
* `__daemon-run` as this uid is a LEAK — a daemon nothing owns.
|
|
8
|
+
*
|
|
9
|
+
* The motivating incident ran 4+ days on yosemite-s1: a headless e2e session
|
|
10
|
+
* launched a daemon under `HOME=/tmp/pin-e2e-<pid>` and never stopped it. The
|
|
11
|
+
* pid-file takeover in `daemon.ts` could not see it because it keeps its own
|
|
12
|
+
* pid file under the temp home, and `agents daemon status`'s duplicate scope
|
|
13
|
+
* (`findSurvivingStateDirDaemons`, RUSH-2368) deliberately covers only this
|
|
14
|
+
* install's state dir — so the leak was invisible to every existing surface.
|
|
15
|
+
*
|
|
16
|
+
* Why flagging a different-HOME daemon here is not the RUSH-2368 harm: that
|
|
17
|
+
* incident taught that a `__daemon-run` under another HOME is not a DUPLICATE
|
|
18
|
+
* of this device's daemon and must never be told to `kill` from the duplicate
|
|
19
|
+
* path. This module does not accuse it of being a duplicate — it reports that
|
|
20
|
+
* NO owner record (unit main PID, recorded daemon.pid) names the pid at all,
|
|
21
|
+
* and shows the process's own HOME and start time so the operator can judge
|
|
22
|
+
* before acting. A different uid is never named: we cannot inspect its
|
|
23
|
+
* environment or signal it.
|
|
24
|
+
*/
|
|
25
|
+
import { execFileSync } from 'child_process';
|
|
26
|
+
import * as fs from 'fs';
|
|
27
|
+
import * as os from 'os';
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import { productionDaemonServiceNames, readDaemonPid, readServiceManagerPid } from './daemon.js';
|
|
30
|
+
import { getDaemonDir } from '../state.js';
|
|
31
|
+
/**
|
|
32
|
+
* The daemon state dir for an ARBITRARY home — the same layout state.ts's
|
|
33
|
+
* DAEMON_DIR chain produces (`<home>/.agents/.cache/helpers/daemon`). A caller
|
|
34
|
+
* under a redirected HOME (a test/e2e harness) uses this to find the REAL
|
|
35
|
+
* install's daemon records. Kept here rather than exported from state.ts: any
|
|
36
|
+
* edit to state.ts selects its entire static-import closure into the required
|
|
37
|
+
* impact gate (186 files, 295s against the 240s budget, measured on this
|
|
38
|
+
* change's first CI run), so the helper cannot live there without a
|
|
39
|
+
* budget-policy change. If state.ts's layout ever moves, this must move with
|
|
40
|
+
* it — the two are the same address.
|
|
41
|
+
*/
|
|
42
|
+
export function getDaemonDirForHome(home) {
|
|
43
|
+
return path.join(home, '.agents', '.cache', 'helpers', 'daemon');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every live `__daemon-run` process on this box, regardless of which install
|
|
47
|
+
* launched it or which state dir it serves. POSIX-only (uses `ps`); a no-op on
|
|
48
|
+
* Windows.
|
|
49
|
+
*
|
|
50
|
+
* `getDaemonLaunch` always spawns `<node> <entry> __daemon-run` with nothing
|
|
51
|
+
* after it — the ONLY argv `__daemon-run` ever appears in for a real daemon.
|
|
52
|
+
* A substring/regex test anywhere in the full command line is not enough: an
|
|
53
|
+
* `agents run claude "<prompt>"` invocation whose prompt happens to quote the
|
|
54
|
+
* literal text `__daemon-run` (this ticket's own brief does) matches that test
|
|
55
|
+
* too, and was observed producing false "duplicate daemon" rows. Requiring it
|
|
56
|
+
* to be the LAST whitespace-delimited token is the actual invariant.
|
|
57
|
+
*
|
|
58
|
+
* This raw box-wide scan is deliberately NOT the duplicate-detection scope
|
|
59
|
+
* (RUSH-2368): a `__daemon-run` under a different HOME serves a different
|
|
60
|
+
* `getDaemonDir()` and is not a duplicate of THIS device's daemon, however
|
|
61
|
+
* `ps` sees it — a leaked vitest fixture under its own `/tmp` HOME matched
|
|
62
|
+
* this scan and was reported as a stray to `kill`. Callers attach their own
|
|
63
|
+
* scope: `agents daemon status` gates on the instance registry,
|
|
64
|
+
* {@link findLeakedDaemons} on the owner records above.
|
|
65
|
+
*/
|
|
66
|
+
export function listDaemonRunProcesses() {
|
|
67
|
+
if (process.platform === 'win32')
|
|
68
|
+
return [];
|
|
69
|
+
let out;
|
|
70
|
+
try {
|
|
71
|
+
out = execFileSync('ps', ['-eo', 'pid=,uid=,args='], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const line of out.split('\n')) {
|
|
78
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
79
|
+
if (!m)
|
|
80
|
+
continue;
|
|
81
|
+
const tokens = m[3].trim().split(/\s+/);
|
|
82
|
+
if (tokens.length === 0 || tokens[tokens.length - 1] !== '__daemon-run')
|
|
83
|
+
continue;
|
|
84
|
+
const pid = parseInt(m[1], 10);
|
|
85
|
+
if (isNaN(pid))
|
|
86
|
+
continue;
|
|
87
|
+
const uidParsed = parseInt(m[2], 10);
|
|
88
|
+
found.push({ pid, uid: isNaN(uidParsed) ? null : uidParsed, tokens });
|
|
89
|
+
}
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The HOME of a live process, or null when unreadable. Linux reads
|
|
94
|
+
* `/proc/<pid>/environ`; macOS has no equivalent zero-dependency primitive, so
|
|
95
|
+
* it falls back to `ps -E`, which appends the environment to the command
|
|
96
|
+
* column. Both are best-effort: a null HOME must never turn into an accusation
|
|
97
|
+
* beyond "unknown".
|
|
98
|
+
*/
|
|
99
|
+
function processHome(pid) {
|
|
100
|
+
if (process.platform === 'linux') {
|
|
101
|
+
try {
|
|
102
|
+
const env = fs.readFileSync(`/proc/${pid}/environ`, 'utf-8');
|
|
103
|
+
for (const entry of env.split('\0')) {
|
|
104
|
+
if (entry.startsWith('HOME='))
|
|
105
|
+
return entry.slice('HOME='.length) || null;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const out = execFileSync('ps', ['-E', '-o', 'command=', '-p', String(pid)], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
115
|
+
const m = out.match(/(?:^|\s)HOME=(\S+)/);
|
|
116
|
+
return m ? m[1] : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** The start time of a live process as `ps lstart` renders it, or null (best-effort, POSIX only). */
|
|
123
|
+
function processStartTime(pid) {
|
|
124
|
+
if (process.platform === 'win32')
|
|
125
|
+
return null;
|
|
126
|
+
try {
|
|
127
|
+
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
128
|
+
return out || null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Every `__daemon-run` process running as THIS uid that no owner record names:
|
|
136
|
+
* neither the service manager's unit main PID nor the recorded
|
|
137
|
+
* `<daemonDir>/daemon.pid`. Each result carries the process's HOME and start
|
|
138
|
+
* time so the report can show them. An empty list means no leak — including
|
|
139
|
+
* the common stopped-daemon case, where there is simply nothing running.
|
|
140
|
+
*/
|
|
141
|
+
export function findLeakedDaemons() {
|
|
142
|
+
const owned = new Set();
|
|
143
|
+
const recorded = readDaemonPid();
|
|
144
|
+
if (recorded)
|
|
145
|
+
owned.add(recorded);
|
|
146
|
+
const unitPid = readServiceManagerPid();
|
|
147
|
+
if (unitPid)
|
|
148
|
+
owned.add(unitPid);
|
|
149
|
+
// A caller under a redirected HOME (a test/e2e harness) must still recognize
|
|
150
|
+
// the REAL install's daemon as owned: its records live under the account
|
|
151
|
+
// home (`os.userInfo().homedir` reads the passwd record, ignoring $HOME),
|
|
152
|
+
// not under this process's HOME. Flagging the box's healthy production
|
|
153
|
+
// daemon as a leak would be the RUSH-2368 harm through a new door.
|
|
154
|
+
const realDaemonDir = getDaemonDirForHome(os.userInfo().homedir);
|
|
155
|
+
if (realDaemonDir !== getDaemonDir()) {
|
|
156
|
+
const realRecorded = readDaemonPid(realDaemonDir);
|
|
157
|
+
if (realRecorded)
|
|
158
|
+
owned.add(realRecorded);
|
|
159
|
+
const realUnitPid = readServiceManagerPid(os.platform(), productionDaemonServiceNames());
|
|
160
|
+
if (realUnitPid)
|
|
161
|
+
owned.add(realUnitPid);
|
|
162
|
+
}
|
|
163
|
+
const myUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
164
|
+
const leaked = [];
|
|
165
|
+
for (const p of listDaemonRunProcesses()) {
|
|
166
|
+
if (owned.has(p.pid))
|
|
167
|
+
continue;
|
|
168
|
+
// Another uid's daemon is never named: we cannot read its environment
|
|
169
|
+
// reliably and could not signal it if we tried.
|
|
170
|
+
if (myUid !== null && p.uid !== null && p.uid !== myUid)
|
|
171
|
+
continue;
|
|
172
|
+
leaked.push({
|
|
173
|
+
pid: p.pid,
|
|
174
|
+
home: processHome(p.pid),
|
|
175
|
+
startedAt: processStartTime(p.pid),
|
|
176
|
+
entry: p.tokens.length >= 2 ? p.tokens[p.tokens.length - 2] : null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return leaked;
|
|
180
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-title service (PHNX-3797) — the ONE generator of session headlines.
|
|
3
|
+
*
|
|
4
|
+
* Every session row's headline used to be the agent's latest transcript line.
|
|
5
|
+
* This service replaces it with a user-anchored NAME: it sweeps the local
|
|
6
|
+
* session index for recent sessions whose title is still the raw first user
|
|
7
|
+
* message, asks a {@link SessionTitleProvider} (the cloud cheap-model call by
|
|
8
|
+
* default) for a short action+object headline, and persists it
|
|
9
|
+
* (`lib/session/title.ts`). Because the value lands in the index, every consumer
|
|
10
|
+
* — the CLI list, the picker, `sessions watch --json`, the fleet mirror, AGI EXT
|
|
11
|
+
* — reads one title that was generated exactly once.
|
|
12
|
+
*
|
|
13
|
+
* Cost is bounded by construction, not by hope: at most
|
|
14
|
+
* {@link SESSION_TITLE_MAX_PER_TICK} generations per tick, only for sessions
|
|
15
|
+
* inside the display window, only for rows whose stored source key no longer
|
|
16
|
+
* matches their user text (so a titled session is a pure DB read forever after),
|
|
17
|
+
* and never for the titler's own spawned sessions. When the harness is missing
|
|
18
|
+
* or signed out every attempt fails, so the service backs off exponentially
|
|
19
|
+
* instead of respawning two processes a minute forever — the rows simply keep
|
|
20
|
+
* showing the user's own words, which is the honest fallback.
|
|
21
|
+
*/
|
|
22
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
23
|
+
import { type SessionTitleRunner } from '../session/title.js';
|
|
24
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
25
|
+
export declare class SessionTitleService extends BasePeriodicService {
|
|
26
|
+
private readonly run?;
|
|
27
|
+
readonly id: DaemonServiceId;
|
|
28
|
+
readonly intervalMs: number;
|
|
29
|
+
readonly deadlineMs = 110000;
|
|
30
|
+
/** Let the box settle (shims, PATH, index warm) before spawning a harness. */
|
|
31
|
+
readonly startupDelayMs = 60000;
|
|
32
|
+
/** Ticks still to skip before the next attempt; 0 = attempt this tick. */
|
|
33
|
+
private skipTicks;
|
|
34
|
+
/** Skip count applied after the next all-failed sweep. */
|
|
35
|
+
private backoff;
|
|
36
|
+
/** `run` is the model-call seam (defaults to the real subprocess); tests inject one. */
|
|
37
|
+
constructor(run?: SessionTitleRunner | undefined);
|
|
38
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
39
|
+
protected onStop(): Promise<void>;
|
|
40
|
+
protected onTick(ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-title service (PHNX-3797) — the ONE generator of session headlines.
|
|
3
|
+
*
|
|
4
|
+
* Every session row's headline used to be the agent's latest transcript line.
|
|
5
|
+
* This service replaces it with a user-anchored NAME: it sweeps the local
|
|
6
|
+
* session index for recent sessions whose title is still the raw first user
|
|
7
|
+
* message, asks a {@link SessionTitleProvider} (the cloud cheap-model call by
|
|
8
|
+
* default) for a short action+object headline, and persists it
|
|
9
|
+
* (`lib/session/title.ts`). Because the value lands in the index, every consumer
|
|
10
|
+
* — the CLI list, the picker, `sessions watch --json`, the fleet mirror, AGI EXT
|
|
11
|
+
* — reads one title that was generated exactly once.
|
|
12
|
+
*
|
|
13
|
+
* Cost is bounded by construction, not by hope: at most
|
|
14
|
+
* {@link SESSION_TITLE_MAX_PER_TICK} generations per tick, only for sessions
|
|
15
|
+
* inside the display window, only for rows whose stored source key no longer
|
|
16
|
+
* matches their user text (so a titled session is a pure DB read forever after),
|
|
17
|
+
* and never for the titler's own spawned sessions. When the harness is missing
|
|
18
|
+
* or signed out every attempt fails, so the service backs off exponentially
|
|
19
|
+
* instead of respawning two processes a minute forever — the rows simply keep
|
|
20
|
+
* showing the user's own words, which is the honest fallback.
|
|
21
|
+
*/
|
|
22
|
+
import { runSessionTitleTick, SESSION_TITLE_MAX_PER_TICK } from '../session/title.js';
|
|
23
|
+
import { BasePeriodicService } from './service.js';
|
|
24
|
+
/** Cadence of the sweep. Slow on purpose: a title is not a live signal. */
|
|
25
|
+
const SESSION_TITLE_TICK_MS = 2 * 60_000;
|
|
26
|
+
/** Above the worst case (limit x the per-generation subprocess timeout), below the interval. */
|
|
27
|
+
const SESSION_TITLE_DEADLINE_MS = 110_000;
|
|
28
|
+
/** Ticks skipped after the first all-failed sweep, doubling up to the ceiling. */
|
|
29
|
+
const SESSION_TITLE_BACKOFF_START = 2;
|
|
30
|
+
/** ~1h at the tick cadence — long enough that a signed-out box stays quiet. */
|
|
31
|
+
const SESSION_TITLE_BACKOFF_MAX = 30;
|
|
32
|
+
export class SessionTitleService extends BasePeriodicService {
|
|
33
|
+
run;
|
|
34
|
+
id = 'session-title';
|
|
35
|
+
intervalMs = SESSION_TITLE_TICK_MS;
|
|
36
|
+
deadlineMs = SESSION_TITLE_DEADLINE_MS;
|
|
37
|
+
/** Let the box settle (shims, PATH, index warm) before spawning a harness. */
|
|
38
|
+
startupDelayMs = 60_000;
|
|
39
|
+
/** Ticks still to skip before the next attempt; 0 = attempt this tick. */
|
|
40
|
+
skipTicks = 0;
|
|
41
|
+
/** Skip count applied after the next all-failed sweep. */
|
|
42
|
+
backoff = 0;
|
|
43
|
+
/** `run` is the model-call seam (defaults to the real subprocess); tests inject one. */
|
|
44
|
+
constructor(run) {
|
|
45
|
+
super();
|
|
46
|
+
this.run = run;
|
|
47
|
+
}
|
|
48
|
+
async onStart(_ctx) { }
|
|
49
|
+
async onStop() { }
|
|
50
|
+
async onTick(ctx, signal) {
|
|
51
|
+
if (this.skipTicks > 0) {
|
|
52
|
+
this.skipTicks--;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const result = await runSessionTitleTick({
|
|
56
|
+
limit: SESSION_TITLE_MAX_PER_TICK,
|
|
57
|
+
signal,
|
|
58
|
+
...(this.run ? { run: this.run } : {}),
|
|
59
|
+
});
|
|
60
|
+
if (result.generated > 0) {
|
|
61
|
+
// A success clears the backoff: whatever was broken is working again.
|
|
62
|
+
this.backoff = 0;
|
|
63
|
+
ctx.log('INFO', `session-title: generated ${result.generated} title(s) (${result.cached} already current)`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (result.failed > 0) {
|
|
67
|
+
this.backoff = this.backoff === 0
|
|
68
|
+
? SESSION_TITLE_BACKOFF_START
|
|
69
|
+
: Math.min(this.backoff * 2, SESSION_TITLE_BACKOFF_MAX);
|
|
70
|
+
this.skipTicks = this.backoff;
|
|
71
|
+
ctx.log('WARN', `session-title: ${result.failed} generation(s) produced no title; backing off ${this.backoff} tick(s)`);
|
|
72
|
+
}
|
|
73
|
+
// Nothing generated and nothing failed = every recent session is titled.
|
|
74
|
+
// That is the steady state, and it logs nothing.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -32,13 +32,21 @@ import type { DaemonServiceId } from '../daemon-services.js';
|
|
|
32
32
|
* freshness gate is the CONSUMER of this producer's cadence: it skips the
|
|
33
33
|
* pushes when the last exchange is older than one usage-sync interval, so it
|
|
34
34
|
* must track this constant rather than its own equal-by-coincidence literal.
|
|
35
|
+
* Sourced from `usage-sync.ts`'s `USAGE_SYNC_INTERVAL_MS` rather than a
|
|
36
|
+
* second literal, so the two files can't drift out of sync with each other.
|
|
35
37
|
*/
|
|
36
38
|
export declare const USAGE_SYNC_TICK_MS: number;
|
|
39
|
+
/**
|
|
40
|
+
* Offset from auth-sync's 60s kickoff so the two never contend for the
|
|
41
|
+
* shared-repo lock. Auth fires at T+1m, T+16m, …; usage at T+8m, T+23m, …
|
|
42
|
+
* The git exchange deadline is 45s, so a 7-minute gap is the whole point.
|
|
43
|
+
*/
|
|
44
|
+
export declare const USAGE_SYNC_KICKOFF_MS: number;
|
|
37
45
|
export declare class UsageSyncService extends BasePeriodicService {
|
|
38
46
|
readonly id: DaemonServiceId;
|
|
39
47
|
readonly intervalMs: number;
|
|
40
48
|
readonly deadlineMs: number;
|
|
41
|
-
readonly startupDelayMs
|
|
49
|
+
readonly startupDelayMs: number;
|
|
42
50
|
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
43
51
|
protected onStop(): Promise<void>;
|
|
44
52
|
protected onTick(ctx: DaemonContext): Promise<void>;
|
|
@@ -26,15 +26,23 @@
|
|
|
26
26
|
* deadline and circuit breaker.
|
|
27
27
|
*/
|
|
28
28
|
import { BasePeriodicService } from './service.js';
|
|
29
|
+
import { USAGE_SYNC_INTERVAL_MS } from '../accounting/usage-sync.js';
|
|
29
30
|
/**
|
|
30
31
|
* The usage-sync tick cadence. Exported because auth-sync's credential-push
|
|
31
32
|
* freshness gate is the CONSUMER of this producer's cadence: it skips the
|
|
32
33
|
* pushes when the last exchange is older than one usage-sync interval, so it
|
|
33
34
|
* must track this constant rather than its own equal-by-coincidence literal.
|
|
35
|
+
* Sourced from `usage-sync.ts`'s `USAGE_SYNC_INTERVAL_MS` rather than a
|
|
36
|
+
* second literal, so the two files can't drift out of sync with each other.
|
|
34
37
|
*/
|
|
35
|
-
export const USAGE_SYNC_TICK_MS =
|
|
38
|
+
export const USAGE_SYNC_TICK_MS = USAGE_SYNC_INTERVAL_MS;
|
|
36
39
|
const USAGE_SYNC_DEADLINE_MS = 2 * 60_000;
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Offset from auth-sync's 60s kickoff so the two never contend for the
|
|
42
|
+
* shared-repo lock. Auth fires at T+1m, T+16m, …; usage at T+8m, T+23m, …
|
|
43
|
+
* The git exchange deadline is 45s, so a 7-minute gap is the whole point.
|
|
44
|
+
*/
|
|
45
|
+
export const USAGE_SYNC_KICKOFF_MS = 8 * 60_000;
|
|
38
46
|
export class UsageSyncService extends BasePeriodicService {
|
|
39
47
|
id = 'usage-sync';
|
|
40
48
|
intervalMs = USAGE_SYNC_TICK_MS;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer' | 'attention-notify' | 'harness-update';
|
|
10
|
+
export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'session-title' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer' | 'attention-notify' | 'harness-update';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -107,6 +107,11 @@ export const DAEMON_SERVICES = [
|
|
|
107
107
|
title: 'Attention desktop banners',
|
|
108
108
|
description: 'Posts one actionable desktop banner per new attention item (question / permission / plan review / stall) so the macOS helper can answer it through agents feed answer (PHNX-4004).',
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
id: 'session-title',
|
|
112
|
+
title: 'Session titles',
|
|
113
|
+
description: 'Generates the short technical title shown as each session row\'s headline, once per session, with a cheap model — so a row is named by what the user asked for, never by the agent\'s latest message.',
|
|
114
|
+
},
|
|
110
115
|
{
|
|
111
116
|
id: 'auth-sync',
|
|
112
117
|
title: 'Auth bundle sync',
|
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -145,6 +145,7 @@ export async function runUsageRefreshTick(signal) {
|
|
|
145
145
|
const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
|
|
146
146
|
const { writeClaudeUsageCache, readClaudeUsageCache } = await import('./accounting/usage.js');
|
|
147
147
|
const { usageRateLimitedUntil } = await import('./usage-backoff.js');
|
|
148
|
+
const { machineId } = await import('./machine-id.js');
|
|
148
149
|
const r = await runUsageRefresh({
|
|
149
150
|
listAccounts: buildLocalUsageAccounts,
|
|
150
151
|
writeUsageCache: writeClaudeUsageCache,
|
|
@@ -156,6 +157,11 @@ export async function runUsageRefreshTick(signal) {
|
|
|
156
157
|
// Thread the supervisor deadline into each provider fetch so the tick's I/O
|
|
157
158
|
// is bounded by deadlineMs, not just each fetch's own 5s timeout (PHNX-3608).
|
|
158
159
|
signal,
|
|
160
|
+
pollerDevice: machineId(),
|
|
161
|
+
onSnapshotsChanged: async () => {
|
|
162
|
+
const { pushUsageSnapshotNow } = await import('./accounting/usage-sync.js');
|
|
163
|
+
await pushUsageSnapshotNow();
|
|
164
|
+
},
|
|
159
165
|
});
|
|
160
166
|
const { listProfiles } = await import('./profiles.js');
|
|
161
167
|
const { refreshDueByokUsage } = await import('./byok-usage.js');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentId } from '../types.js';
|
|
2
2
|
import type { DuplicateVersionHook } from '../hooks/install.js';
|
|
3
3
|
import type { AgentsBinaryShadow } from '../binary-shadow.js';
|
|
4
|
+
import type { LeakedDaemon } from '../daemon/leaked-daemons.js';
|
|
4
5
|
import type { RcSecretFinding } from '../secrets-types.js';
|
|
5
6
|
import type { OwnerSinkStatus } from '../channels/owner-sink.js';
|
|
6
7
|
import { type WindowsSshEnrollmentAudit } from './windows-ssh-enrollment.js';
|
|
@@ -13,7 +14,7 @@ export type FindingSeverity = 'critical' | 'warning';
|
|
|
13
14
|
* the JSON consumer group by kind. */
|
|
14
15
|
/** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
|
|
15
16
|
* below owns it, and a second copy in these comments is a fourth place to drift. */
|
|
16
|
-
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "auth-bundle-wrong-backend", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable"];
|
|
17
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "hook-runtime-broken", "hook-runtime-visibility-unavailable", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "auth-bundle-wrong-backend", "exec-policy", "ssh-key-enrollment", "stale-cli", "binary-shadow", "owner-sink-unreachable", "leaked-daemon"];
|
|
17
18
|
/**
|
|
18
19
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
19
20
|
* the builders below and asserted against both prose rubrics by
|
|
@@ -124,6 +125,11 @@ export interface LocalFindingInputs {
|
|
|
124
125
|
ownerSink?: OwnerSinkStatus;
|
|
125
126
|
/** `agents` binaries that shadow the currently running CLI (RUSH-2431). */
|
|
126
127
|
binaryShadows?: AgentsBinaryShadow[];
|
|
128
|
+
/** `__daemon-run` processes no owner record names — neither the service
|
|
129
|
+
* manager's unit main PID nor the recorded daemon.pid (W4, PHNX-3736).
|
|
130
|
+
* Collected by `findLeakedDaemons` in the command (it runs `ps` and reads
|
|
131
|
+
* /proc, so it stays out of this pure module). */
|
|
132
|
+
leakedDaemons?: LeakedDaemon[];
|
|
127
133
|
}
|
|
128
134
|
/**
|
|
129
135
|
* Fold this machine's signals into findings. Missing hooks/plugins and unwired
|