@phnx-labs/agents-cli 1.22.29 → 1.22.31
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 +88 -0
- package/README.md +44 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +86 -105
- package/dist/commands/sessions-bookmark.d.ts +20 -0
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +21 -8
- package/dist/commands/sessions.js +328 -74
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +13 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -1
- package/dist/lib/secrets/agent.js +32 -6
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
- package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
- package/dist/commands/sessions-favorite.d.ts +0 -20
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-subsystem health record for the always-on daemon.
|
|
3
|
+
*
|
|
4
|
+
* Today a subsystem failure inside `runDaemon()` (daemon.ts) is a single
|
|
5
|
+
* `log('ERROR', ...)` line that scrolls out of the log file and is never
|
|
6
|
+
* surfaced anywhere else — `agents daemon status` has no way to answer "is the
|
|
7
|
+
* secrets broker actually healthy right now?" beyond "the daemon process is
|
|
8
|
+
* alive". This module gives every subsystem a small persisted record —
|
|
9
|
+
* {@link SubsystemHealth} — so `agents daemon status` / `agents daemon
|
|
10
|
+
* services` can report health, not just liveness (RUSH-2354).
|
|
11
|
+
*
|
|
12
|
+
* Scheduled routines get this for free once migrated onto `agents routines`
|
|
13
|
+
* (their run history already carries success/failure — `agents routines
|
|
14
|
+
* stats`). This module exists for the two subsystems that predate routines
|
|
15
|
+
* and have no run history of their own: the secrets broker and the browser
|
|
16
|
+
* IPC server.
|
|
17
|
+
*
|
|
18
|
+
* File-backed (one JSON object keyed by subsystem name) rather than in-memory
|
|
19
|
+
* because `agents daemon status` runs as a SEPARATE process from the daemon —
|
|
20
|
+
* it must read what the daemon last recorded, not maintain its own state.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { getDaemonDir } from './state.js';
|
|
25
|
+
const HEALTH_FILE = 'health.json';
|
|
26
|
+
/** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
|
|
27
|
+
export const SUBSYSTEM_SECRETS_BROKER = 'secrets-broker';
|
|
28
|
+
export const SUBSYSTEM_BROWSER_IPC = 'browser-ipc';
|
|
29
|
+
function getHealthPath() {
|
|
30
|
+
return path.join(getDaemonDir(), HEALTH_FILE);
|
|
31
|
+
}
|
|
32
|
+
function readAll() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(getHealthPath(), 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function writeAll(records) {
|
|
46
|
+
const healthPath = getHealthPath();
|
|
47
|
+
fs.mkdirSync(path.dirname(healthPath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(healthPath, JSON.stringify(records), 'utf-8');
|
|
49
|
+
try {
|
|
50
|
+
fs.chmodSync(healthPath, 0o600);
|
|
51
|
+
}
|
|
52
|
+
catch { /* best effort */ }
|
|
53
|
+
}
|
|
54
|
+
function blankRecord(subsystem) {
|
|
55
|
+
return { subsystem, lastError: null, lastErrorAt: null, consecutiveFailures: 0, lastOkAt: null };
|
|
56
|
+
}
|
|
57
|
+
/** Record a successful subsystem check-in — clears the failure streak. */
|
|
58
|
+
export function recordSubsystemOk(subsystem, at = new Date().toISOString()) {
|
|
59
|
+
const all = readAll();
|
|
60
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
61
|
+
all[subsystem] = { ...existing, subsystem, consecutiveFailures: 0, lastOkAt: at };
|
|
62
|
+
writeAll(all);
|
|
63
|
+
}
|
|
64
|
+
/** Record a subsystem failure — bumps the consecutive-failure streak. */
|
|
65
|
+
export function recordSubsystemError(subsystem, error, at = new Date().toISOString()) {
|
|
66
|
+
const all = readAll();
|
|
67
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
68
|
+
all[subsystem] = {
|
|
69
|
+
...existing,
|
|
70
|
+
subsystem,
|
|
71
|
+
lastError: error,
|
|
72
|
+
lastErrorAt: at,
|
|
73
|
+
consecutiveFailures: existing.consecutiveFailures + 1,
|
|
74
|
+
};
|
|
75
|
+
writeAll(all);
|
|
76
|
+
}
|
|
77
|
+
/** Read one subsystem's health record, or null if it has never reported in. */
|
|
78
|
+
export function readSubsystemHealth(subsystem) {
|
|
79
|
+
return readAll()[subsystem] ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** Read every subsystem's health record, sorted by subsystem name. */
|
|
82
|
+
export function readAllSubsystemHealth() {
|
|
83
|
+
return Object.values(readAll()).sort((a, b) => a.subsystem.localeCompare(b.subsystem));
|
|
84
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon housekeeping ticks — one-shot bodies invoked by system routines.
|
|
3
|
+
*
|
|
4
|
+
* RUSH-2353: these were ~15 hardcoded `setInterval` timers inside
|
|
5
|
+
* `runDaemon()` (daemon.ts) — a parallel, inferior reimplementation of the
|
|
6
|
+
* routines system (no declaration, no run history, no pause/disable, no
|
|
7
|
+
* device pin). Each function here is one migrated tick's *body*, unchanged in
|
|
8
|
+
* behavior, now invoked as a detached one-shot process by a system routine's
|
|
9
|
+
* `command:` (via the `agents __daemon-tick <name>` entrypoint in index.ts)
|
|
10
|
+
* instead of an in-process interval closure.
|
|
11
|
+
*
|
|
12
|
+
* Overlap protection that used to live in daemon.ts as a per-tick boolean
|
|
13
|
+
* flag (`watchdogInFlight`, `probingDevices`, ...) is now provided by the
|
|
14
|
+
* routine runner's launch claim (`withRoutineLaunchClaim` in runner.ts) — two
|
|
15
|
+
* fires of the same routine name never run concurrently.
|
|
16
|
+
*
|
|
17
|
+
* Output goes to `console.log`/`console.error`: each tick runs as a spawned
|
|
18
|
+
* child of `executeCommandJobDetached`, which redirects the child's stdout to
|
|
19
|
+
* the run's `stdout.log` — so this doubles as the run's log, readable via
|
|
20
|
+
* `agents routines runs <name>`.
|
|
21
|
+
*/
|
|
22
|
+
/** ~every 3 min. Mirrors the old WATCHDOG_TICK_MS. */
|
|
23
|
+
export declare function runWatchdogTick(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Device probe: refresh registered devices' reachability and detect newly
|
|
26
|
+
* appeared tailnet nodes, dropping a sentinel per pending device so the
|
|
27
|
+
* menu-bar helper can surface "NEW DEVICES -> Register / Ignore". Refresh
|
|
28
|
+
* mode never auto-registers a newcomer. A machine without tailscale is a
|
|
29
|
+
* clean no-op. ~every 3 min.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runDeviceProbeTick(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* tmux hook reconcile: retrofit the guarded `pane-died` hook onto managed
|
|
34
|
+
* `agents run` sessions a pre-fix binary left with the old unconditional
|
|
35
|
+
* hook. Non-destructive: set-hook only, never a kill or detach. ~every 5 min.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runTmuxReconcileTick(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Launch-health self-heal: probe that each agent's DEFAULT version actually
|
|
40
|
+
* LAUNCHES, and repair a gutted install. Never repoints the global default
|
|
41
|
+
* (allowDefaultSwitch: false) — a background default switch would be a
|
|
42
|
+
* silent logout. ~every 6h.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runLaunchHealthTick(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
47
|
+
* status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
|
|
48
|
+
* ~every 3 min.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runFleetCacheWarmTick(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Session-status cache warm (RUSH-2062): publish THIS host's local active
|
|
53
|
+
* sessions so menubar / Factory / watchdog / CLI share one warm snapshot.
|
|
54
|
+
* Publish-own only (no cross-host SSH). ~every 3 min.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runSessionCacheWarmTick(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
59
|
+
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
60
|
+
* This host is the sole writer for its own local accounts. ~every 60s
|
|
61
|
+
* (USAGE_REFRESH_TICK_MS in usage-refresh.ts — keep in sync).
|
|
62
|
+
*/
|
|
63
|
+
export declare function runUsageRefreshTick(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Auto-dispatch: for any managed project that has opted in (autoDispatch:true
|
|
66
|
+
* + maxAgents>0 in ~/.agents/factory/projects.json), pick up Linear tickets
|
|
67
|
+
* delegated to an agent and still in Todo, and dispatch each through
|
|
68
|
+
* agents-cli's own cloud-provider layer. OFF unless a project opts in; no
|
|
69
|
+
* opted-in project or no LINEAR_API_KEY is a clean no-op. ~every 3 min.
|
|
70
|
+
*
|
|
71
|
+
* Migrated to a routine (RUSH-2353) so it inherits the `devices:` allowlist —
|
|
72
|
+
* pin with `agents routines devices auto-dispatch --set <one>` to fix the
|
|
73
|
+
* shared-input double-fire problem this job had hardcoded (every daemon on
|
|
74
|
+
* the fleet polled the same Linear queue with no coordination).
|
|
75
|
+
*/
|
|
76
|
+
export declare function runAutoDispatchTick(): Promise<void>;
|
|
77
|
+
/** Registry: routine-facing name -> tick body. Keys match the shipped routine YAML names. */
|
|
78
|
+
export declare const DAEMON_TICKS: Record<string, () => Promise<void>>;
|
|
79
|
+
export declare const DAEMON_TICK_ROUTINE_NAMES: readonly string[];
|
|
80
|
+
/** Run one named tick, or throw for an unknown name (fails the routine run loud). */
|
|
81
|
+
export declare function runDaemonTick(name: string): Promise<void>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon housekeeping ticks — one-shot bodies invoked by system routines.
|
|
3
|
+
*
|
|
4
|
+
* RUSH-2353: these were ~15 hardcoded `setInterval` timers inside
|
|
5
|
+
* `runDaemon()` (daemon.ts) — a parallel, inferior reimplementation of the
|
|
6
|
+
* routines system (no declaration, no run history, no pause/disable, no
|
|
7
|
+
* device pin). Each function here is one migrated tick's *body*, unchanged in
|
|
8
|
+
* behavior, now invoked as a detached one-shot process by a system routine's
|
|
9
|
+
* `command:` (via the `agents __daemon-tick <name>` entrypoint in index.ts)
|
|
10
|
+
* instead of an in-process interval closure.
|
|
11
|
+
*
|
|
12
|
+
* Overlap protection that used to live in daemon.ts as a per-tick boolean
|
|
13
|
+
* flag (`watchdogInFlight`, `probingDevices`, ...) is now provided by the
|
|
14
|
+
* routine runner's launch claim (`withRoutineLaunchClaim` in runner.ts) — two
|
|
15
|
+
* fires of the same routine name never run concurrently.
|
|
16
|
+
*
|
|
17
|
+
* Output goes to `console.log`/`console.error`: each tick runs as a spawned
|
|
18
|
+
* child of `executeCommandJobDetached`, which redirects the child's stdout to
|
|
19
|
+
* the run's `stdout.log` — so this doubles as the run's log, readable via
|
|
20
|
+
* `agents routines runs <name>`.
|
|
21
|
+
*/
|
|
22
|
+
import { getConfigValue } from './device-config.js';
|
|
23
|
+
/** ~every 3 min. Mirrors the old WATCHDOG_TICK_MS. */
|
|
24
|
+
export async function runWatchdogTick() {
|
|
25
|
+
if (getConfigValue('watchdog.enabled').value !== true) {
|
|
26
|
+
console.log('watchdog: disabled (watchdog.enabled != true) — skipping');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const { runWatchdogPass } = await import('./watchdog/service.js');
|
|
30
|
+
const result = await runWatchdogPass({ nudge: true });
|
|
31
|
+
console.log(`watchdog: ${result.counts.total} live, ${result.counts.stalled} stalled, ${result.counts.nudged} nudged`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Device probe: refresh registered devices' reachability and detect newly
|
|
35
|
+
* appeared tailnet nodes, dropping a sentinel per pending device so the
|
|
36
|
+
* menu-bar helper can surface "NEW DEVICES -> Register / Ignore". Refresh
|
|
37
|
+
* mode never auto-registers a newcomer. A machine without tailscale is a
|
|
38
|
+
* clean no-op. ~every 3 min.
|
|
39
|
+
*/
|
|
40
|
+
export async function runDeviceProbeTick() {
|
|
41
|
+
const { runDeviceSync } = await import('./devices/sync.js');
|
|
42
|
+
const { reconcilePendingSentinels } = await import('./devices/pending.js');
|
|
43
|
+
const dev = await runDeviceSync({ soft: true, mode: 'refresh' });
|
|
44
|
+
if (!dev.ok) {
|
|
45
|
+
console.log('device probe: sync not ok — skipping sentinel reconcile');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
reconcilePendingSentinels(dev.pending);
|
|
49
|
+
if (dev.pending.length) {
|
|
50
|
+
console.log(`devices: ${dev.pending.length} new pending (${dev.pending.map((p) => p.name).join(', ')})`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log('device probe: no new pending devices');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* tmux hook reconcile: retrofit the guarded `pane-died` hook onto managed
|
|
58
|
+
* `agents run` sessions a pre-fix binary left with the old unconditional
|
|
59
|
+
* hook. Non-destructive: set-hook only, never a kill or detach. ~every 5 min.
|
|
60
|
+
*/
|
|
61
|
+
export async function runTmuxReconcileTick() {
|
|
62
|
+
const { isTmuxInstalled } = await import('./tmux/binary.js');
|
|
63
|
+
if (!isTmuxInstalled()) {
|
|
64
|
+
console.log('tmux reconcile: tmux not installed — skipping');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const { reconcileSessionHooks } = await import('./tmux/session.js');
|
|
68
|
+
const r = await reconcileSessionHooks();
|
|
69
|
+
console.log(`tmux: retrofitted pane-died hook on ${r.reconciled} session(s)`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Launch-health self-heal: probe that each agent's DEFAULT version actually
|
|
73
|
+
* LAUNCHES, and repair a gutted install. Never repoints the global default
|
|
74
|
+
* (allowDefaultSwitch: false) — a background default switch would be a
|
|
75
|
+
* silent logout. ~every 6h.
|
|
76
|
+
*/
|
|
77
|
+
export async function runLaunchHealthTick() {
|
|
78
|
+
const { healBrokenDefaultLaunches } = await import('./versions.js');
|
|
79
|
+
const { repaired, unhealed } = await healBrokenDefaultLaunches((m) => console.log(`launch-health: ${m}`), { allowDefaultSwitch: false });
|
|
80
|
+
if (repaired.length)
|
|
81
|
+
console.log(`launch-health: repaired ${repaired.join(', ')}`);
|
|
82
|
+
if (unhealed.length) {
|
|
83
|
+
console.log(`launch-health: ${unhealed.join(', ')} won't launch and will not be auto-switched — choose a version with \`agents use <agent> <version>\` or \`agents add <agent>@latest\``);
|
|
84
|
+
}
|
|
85
|
+
if (!repaired.length && !unhealed.length)
|
|
86
|
+
console.log('launch-health: all default launches healthy');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
90
|
+
* status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
|
|
91
|
+
* ~every 3 min.
|
|
92
|
+
*/
|
|
93
|
+
export async function runFleetCacheWarmTick() {
|
|
94
|
+
const { machineId } = await import('./machine-id.js');
|
|
95
|
+
const self = machineId();
|
|
96
|
+
const { probeLocalFleetAuth, writeFleetAuthRows } = await import('./auth-health.js');
|
|
97
|
+
const { getCliVersion } = await import('./version.js');
|
|
98
|
+
const authRows = await probeLocalFleetAuth({ cliVersion: getCliVersion() });
|
|
99
|
+
writeFleetAuthRows(self, authRows);
|
|
100
|
+
const { publishLocalFleetStatus } = await import('./fleet-status.js');
|
|
101
|
+
const row = await publishLocalFleetStatus(self);
|
|
102
|
+
console.log(`fleet cache warm: ${authRows.length} auth row(s), ${row.agents.running} running agent(s) on ${self}`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Session-status cache warm (RUSH-2062): publish THIS host's local active
|
|
106
|
+
* sessions so menubar / Factory / watchdog / CLI share one warm snapshot.
|
|
107
|
+
* Publish-own only (no cross-host SSH). ~every 3 min.
|
|
108
|
+
*/
|
|
109
|
+
export async function runSessionCacheWarmTick() {
|
|
110
|
+
const { publishLocalActiveSessions } = await import('./session/session-cache.js');
|
|
111
|
+
const r = await publishLocalActiveSessions();
|
|
112
|
+
console.log(`session cache warm: ${r.sessions.length} local session(s)`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
116
|
+
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
117
|
+
* This host is the sole writer for its own local accounts. ~every 60s
|
|
118
|
+
* (USAGE_REFRESH_TICK_MS in usage-refresh.ts — keep in sync).
|
|
119
|
+
*/
|
|
120
|
+
export async function runUsageRefreshTick() {
|
|
121
|
+
const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
|
|
122
|
+
const { writeClaudeUsageCache } = await import('./usage.js');
|
|
123
|
+
const { usageRateLimitedUntil } = await import('./usage-backoff.js');
|
|
124
|
+
const r = await runUsageRefresh({
|
|
125
|
+
listAccounts: buildLocalUsageAccounts,
|
|
126
|
+
writeUsageCache: writeClaudeUsageCache,
|
|
127
|
+
backoffUntil: usageRateLimitedUntil,
|
|
128
|
+
});
|
|
129
|
+
console.log(`usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Auto-dispatch: for any managed project that has opted in (autoDispatch:true
|
|
133
|
+
* + maxAgents>0 in ~/.agents/factory/projects.json), pick up Linear tickets
|
|
134
|
+
* delegated to an agent and still in Todo, and dispatch each through
|
|
135
|
+
* agents-cli's own cloud-provider layer. OFF unless a project opts in; no
|
|
136
|
+
* opted-in project or no LINEAR_API_KEY is a clean no-op. ~every 3 min.
|
|
137
|
+
*
|
|
138
|
+
* Migrated to a routine (RUSH-2353) so it inherits the `devices:` allowlist —
|
|
139
|
+
* pin with `agents routines devices auto-dispatch --set <one>` to fix the
|
|
140
|
+
* shared-input double-fire problem this job had hardcoded (every daemon on
|
|
141
|
+
* the fleet polled the same Linear queue with no coordination).
|
|
142
|
+
*/
|
|
143
|
+
export async function runAutoDispatchTick() {
|
|
144
|
+
const { readAutoDispatchProjects, isEligible, autoDispatchTick } = await import('./auto-dispatch.js');
|
|
145
|
+
const projects = readAutoDispatchProjects();
|
|
146
|
+
if (!projects.some(isEligible)) {
|
|
147
|
+
console.log('auto-dispatch: no opted-in project — skipping');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const { createLinearGateway } = await import('./auto-dispatch-linear.js');
|
|
151
|
+
const linear = createLinearGateway();
|
|
152
|
+
if (!linear) {
|
|
153
|
+
console.log('auto-dispatch: no LINEAR_API_KEY configured — skipping');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const { createProviderDispatcher } = await import('./auto-dispatch-provider.js');
|
|
157
|
+
const dispatcher = createProviderDispatcher();
|
|
158
|
+
const dispatched = await autoDispatchTick({
|
|
159
|
+
projects,
|
|
160
|
+
linear,
|
|
161
|
+
dispatcher,
|
|
162
|
+
log: (lvl, m) => (lvl === 'ERROR' ? console.error(m) : console.log(m)),
|
|
163
|
+
});
|
|
164
|
+
if (dispatched.length) {
|
|
165
|
+
console.log(`auto-dispatch: started ${dispatched.length} delegated ticket(s): ${dispatched.map((d) => d.identifier).join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log('auto-dispatch: no delegated tickets to dispatch');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Registry: routine-facing name -> tick body. Keys match the shipped routine YAML names. */
|
|
172
|
+
export const DAEMON_TICKS = {
|
|
173
|
+
watchdog: runWatchdogTick,
|
|
174
|
+
'device-probe': runDeviceProbeTick,
|
|
175
|
+
'tmux-reconcile': runTmuxReconcileTick,
|
|
176
|
+
'launch-health': runLaunchHealthTick,
|
|
177
|
+
'fleet-cache-warm': runFleetCacheWarmTick,
|
|
178
|
+
'session-cache-warm': runSessionCacheWarmTick,
|
|
179
|
+
'usage-refresh': runUsageRefreshTick,
|
|
180
|
+
'auto-dispatch': runAutoDispatchTick,
|
|
181
|
+
};
|
|
182
|
+
export const DAEMON_TICK_ROUTINE_NAMES = Object.freeze(Object.keys(DAEMON_TICKS));
|
|
183
|
+
/** Run one named tick, or throw for an unknown name (fails the routine run loud). */
|
|
184
|
+
export async function runDaemonTick(name) {
|
|
185
|
+
const fn = DAEMON_TICKS[name];
|
|
186
|
+
if (!fn) {
|
|
187
|
+
throw new Error(`Unknown daemon tick '${name}'. Known: ${Object.keys(DAEMON_TICKS).join(', ')}`);
|
|
188
|
+
}
|
|
189
|
+
await fn();
|
|
190
|
+
}
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -58,25 +58,45 @@ export declare function isDaemonRunning(): boolean;
|
|
|
58
58
|
* writeDaemonPid() unconditionally, clobber a live daemon's recorded PID, and
|
|
59
59
|
* run a second JobScheduler concurrently, so every cron routine fires twice.
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* LAST-WINS takeover (SING-11, RUSH-2352): when a live daemon already owns the
|
|
62
|
+
* pid file, this does NOT defer to it — it evicts the incumbent and becomes the
|
|
63
|
+
* survivor, so a second install can never leave two daemons running. Returns true
|
|
64
|
+
* and records our PID once the incumbent is provably dead (its resources
|
|
65
|
+
* released). Returns false ONLY when another `__daemon-run` currently holds the
|
|
66
|
+
* O_EXCL start lock — i.e. a concurrent claimer is mid-takeover and will be the
|
|
67
|
+
* singleton — in which case the caller must exit without touching further state.
|
|
68
|
+
* The read-evict-write is serialized behind the same start lock startDaemon()
|
|
69
|
+
* uses, so two `_run` processes can't both claim in the window between the
|
|
70
|
+
* liveness check and the write.
|
|
66
71
|
*/
|
|
67
72
|
export declare function claimDaemonInstance(): boolean;
|
|
68
73
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* reaper
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
74
|
+
* Record this daemon in the device's instance registry — a marker file named by
|
|
75
|
+
* pid under `<daemonDir>/instances/`. The registry, not a process scan, is how
|
|
76
|
+
* the reaper enumerates the device singleton: because the dir lives INSIDE the
|
|
77
|
+
* daemon dir (`AGENTS_DAEMON_DIR` ?? `<HOME>/.agents/.cache/helpers/daemon`), every
|
|
78
|
+
* daemon of one device — however it was launched — registers in the same place,
|
|
79
|
+
* while a genuinely separate install/home or a test fixture registers under its
|
|
80
|
+
* own daemon dir and is invisible here. This is what fixes the two-entry pile-up:
|
|
81
|
+
* the compiled `dist/bin/agents` binary and the `node <shim>` JS entry have
|
|
82
|
+
* different `process.argv[1]`, so the old launch-entry-scoped `ps` match never
|
|
83
|
+
* reaped across them and duplicates accumulated (78 observed on one box), every
|
|
84
|
+
* routine double-firing. Best-effort — the reaper self-heals a missing/stale
|
|
85
|
+
* marker, and reading another process's ENV to key on the daemon dir directly is
|
|
86
|
+
* not portable (hardened macOS hides it from `ps`), so identity rides the shared
|
|
87
|
+
* on-disk registry instead. No-op on Windows (POSIX-only reaper).
|
|
88
|
+
*/
|
|
89
|
+
export declare function registerDaemonInstance(pid?: number): void;
|
|
90
|
+
/** Remove this daemon's registry marker on graceful shutdown. */
|
|
91
|
+
export declare function unregisterDaemonInstance(pid?: number): void;
|
|
92
|
+
/**
|
|
93
|
+
* Reap stray duplicate daemons of THIS device — every registrant in the instance
|
|
94
|
+
* registry that is a live `agents __daemon-run` and is neither this process nor
|
|
95
|
+
* the current pid-file owner. A predecessor SIGKILLed/OOM-ed without cleanup, or a
|
|
96
|
+
* duplicate that lost the pid-file write race, would otherwise keep a second
|
|
97
|
+
* scheduler alive and double-fire jobs even after claimDaemonInstance() hands the
|
|
98
|
+
* pid file to the survivor. Also garbage-collects markers whose pid is dead or was
|
|
99
|
+
* reused by an unrelated process. No-op on Windows (POSIX-only).
|
|
80
100
|
*/
|
|
81
101
|
export declare function reapStrayDaemons(keepPid?: number): {
|
|
82
102
|
reaped: number;
|
|
@@ -236,8 +256,38 @@ export declare function startDetached(opts?: StartDetachedOptions): {
|
|
|
236
256
|
pid: number | null;
|
|
237
257
|
method: string;
|
|
238
258
|
};
|
|
239
|
-
/**
|
|
240
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Structured outcome of {@link stopDaemon} (SING-12, RUSH-2355). `stopDaemon`
|
|
261
|
+
* asserts its postcondition instead of assuming it: `ok` is true only when every
|
|
262
|
+
* resource the daemon held is provably released. `surviving` names anything that
|
|
263
|
+
* did not release (a still-live daemon, or a stale socket that could not be
|
|
264
|
+
* cleared) and is what drives a non-zero exit; `detachedChildren` are the
|
|
265
|
+
* in-flight routine children that survive deliberately (SING-11a) and are
|
|
266
|
+
* reported, never killed.
|
|
267
|
+
*/
|
|
268
|
+
export interface DaemonStopResult {
|
|
269
|
+
ok: boolean;
|
|
270
|
+
stoppedPid: number | null;
|
|
271
|
+
escalated: boolean;
|
|
272
|
+
released: string[];
|
|
273
|
+
surviving: string[];
|
|
274
|
+
detachedChildren: number[];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Stop the daemon and ASSERT its postcondition (SING-12, RUSH-2355), unloading it
|
|
278
|
+
* from launchd/systemd if applicable.
|
|
279
|
+
*
|
|
280
|
+
* The SIGTERM → grace → killTree sequence is unchanged; what it adds is
|
|
281
|
+
* verification. After the daemon is gone it checks that the secrets broker socket
|
|
282
|
+
* and browser IPC binding actually released — a stale socket present on disk but
|
|
283
|
+
* with no live owner is the orphan that keeps clients holding unlocked bundles
|
|
284
|
+
* hanging (`daemon.ts` broker-hosting; the two-brokers-on-one-socket bug) — and
|
|
285
|
+
* that no `__daemon-run` for THIS state dir survives. A killTree escalation exits
|
|
286
|
+
* without running the daemon's graceful handleShutdown, so those sockets can be
|
|
287
|
+
* left stale; this reclaims each (the owner is provably dead) and reports it. It
|
|
288
|
+
* never reports success on an unverified stop.
|
|
289
|
+
*/
|
|
290
|
+
export declare function stopDaemon(): DaemonStopResult;
|
|
241
291
|
/** Get current daemon status including running state, PID, and enabled job count. */
|
|
242
292
|
export declare function getDaemonStatus(): {
|
|
243
293
|
state: 'running' | 'wedged' | 'stopped';
|