@phnx-labs/agents-cli 1.22.69 → 1.22.71
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 +34 -0
- package/README.md +34 -9
- package/dist/bootstrap.js +4 -4
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +2 -2
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/repo.js +2 -2
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +9 -3
- package/dist/commands/sessions-export.d.ts +5 -1
- package/dist/commands/sessions-export.js +100 -24
- package/dist/commands/sessions-import.d.ts +2 -1
- package/dist/commands/sessions-import.js +85 -21
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage-sync.d.ts +1 -1
- package/dist/lib/accounting/usage-sync.js +3 -3
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/browser/ipc.d.ts +34 -0
- package/dist/lib/browser/ipc.js +149 -20
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/browser/types.d.ts +3 -1
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/auth-sync-service.js +1 -1
- package/dist/lib/daemon/browser-task-reap-service.js +1 -1
- package/dist/lib/daemon/daemon.js +41 -11
- package/dist/lib/daemon/heartbeat-service.js +3 -3
- package/dist/lib/daemon/keychain-reap-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +237 -80
- package/dist/lib/daemon/self-heal-service.js +13 -3
- package/dist/lib/daemon/self-update-service.d.ts +174 -0
- package/dist/lib/daemon/self-update-service.js +353 -0
- package/dist/lib/daemon/state-dir-check-service.js +3 -3
- package/dist/lib/daemon/usage-sync-service.js +1 -1
- package/dist/lib/daemon/watchdog-service.js +4 -4
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +12 -1
- package/dist/lib/device-config.js +63 -13
- package/dist/lib/exec-bounded.d.ts +52 -0
- package/dist/lib/exec-bounded.js +113 -0
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +3 -34
- package/dist/lib/feed/events.d.ts +22 -14
- package/dist/lib/feed/events.js +84 -44
- package/dist/lib/feed-broadcast.d.ts +12 -29
- package/dist/lib/feed-broadcast.js +28 -27
- package/dist/lib/fleet-shared-state.d.ts +12 -5
- package/dist/lib/fleet-shared-state.js +50 -20
- package/dist/lib/fs-atomic.d.ts +11 -0
- package/dist/lib/fs-atomic.js +60 -0
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/hosts/reconcile.d.ts +11 -4
- package/dist/lib/hosts/reconcile.js +31 -5
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +46 -3
- package/dist/lib/owner-message.js +26 -6
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/project-resources.d.ts +12 -0
- package/dist/lib/project-resources.js +129 -0
- package/dist/lib/routine-process-cleanup.d.ts +2 -2
- package/dist/lib/routine-process-cleanup.js +45 -34
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/reaper.d.ts +2 -2
- package/dist/lib/secrets/reaper.js +13 -10
- package/dist/lib/secrets/reserved-sync.d.ts +1 -1
- package/dist/lib/secrets/reserved-sync.js +4 -4
- package/dist/lib/self-update.d.ts +21 -8
- package/dist/lib/self-update.js +54 -31
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/sync/backend.d.ts +61 -0
- package/dist/lib/session/sync/backend.js +89 -0
- package/dist/lib/session/sync/managed-config.d.ts +29 -0
- package/dist/lib/session/sync/managed-config.js +23 -0
- package/dist/lib/session/sync/managed-key.d.ts +45 -0
- package/dist/lib/session/sync/managed-key.js +128 -0
- package/dist/lib/session/sync/net-client.d.ts +65 -0
- package/dist/lib/session/sync/net-client.js +117 -0
- package/dist/lib/session/sync/provision.d.ts +19 -0
- package/dist/lib/session/sync/provision.js +38 -0
- package/dist/lib/session/sync/r2.d.ts +5 -2
- package/dist/lib/session/sync/r2.js +5 -2
- package/dist/lib/session/sync/worker-template.d.ts +6 -0
- package/dist/lib/session/sync/worker-template.js +847 -0
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/tmux/orphan-reap.js +6 -4
- package/dist/lib/tmux/session.js +4 -1
- package/dist/lib/traces/classify.d.ts +8 -1
- package/dist/lib/traces/insights.d.ts +13 -1
- package/dist/lib/traces/insights.js +78 -3
- package/dist/lib/traces/sync.js +8 -3
- package/dist/lib/traces/worker-template.js +9 -5
- package/dist/lib/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { BasePeriodicService } from './service.js';
|
|
18
18
|
import { getDaemonDir } from '../state.js';
|
|
19
|
-
import * as
|
|
19
|
+
import * as fsp from 'fs/promises';
|
|
20
|
+
/** Async `existsSync` — never a synchronous stat on the daemon tick loop (PHNX-3695). */
|
|
21
|
+
async function pathExists(p) {
|
|
22
|
+
try {
|
|
23
|
+
await fsp.access(p);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
20
30
|
/** Matches the historical inline interval (daemon.ts SELF_HEAL_TICK_MS). Runs ~every 6h. */
|
|
21
31
|
const SELF_HEAL_TICK_MS = 6 * 60 * 60_000;
|
|
22
32
|
/** Hard cap per tick — a full resource repair sweep across every version home, short enough a hang never freezes the service for long relative to its 6h cadence. */
|
|
@@ -35,10 +45,10 @@ export class SelfHealService extends BasePeriodicService {
|
|
|
35
45
|
// Nothing to release — the supervisor's timer teardown is the only cleanup needed.
|
|
36
46
|
}
|
|
37
47
|
async onTick(ctx) {
|
|
38
|
-
if (!
|
|
48
|
+
if (!(await pathExists(getDaemonDir())))
|
|
39
49
|
return;
|
|
40
50
|
const { runSelfHeal, selfHealChangedAnything, selfHealNeedsAttention, summarizeSelfHeal } = await import('../self-heal/registry.js');
|
|
41
|
-
if (!
|
|
51
|
+
if (!(await pathExists(getDaemonDir())))
|
|
42
52
|
return;
|
|
43
53
|
const report = await runSelfHeal({ mode: 'safe' });
|
|
44
54
|
if (selfHealChangedAnything(report) || selfHealNeedsAttention(report)) {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon self-update service (PHNX-3695, "Fix 2").
|
|
3
|
+
*
|
|
4
|
+
* The daemon (`agents __daemon-run`) is a long-running background process that
|
|
5
|
+
* historically opted OUT of the CLI's interactive auto-update: `bootstrap.ts`
|
|
6
|
+
* force-sets `AGENTS_CLI_DISABLE_AUTO_UPDATE=1` for `__daemon-run`, so a
|
|
7
|
+
* running daemon never picked up new agents-cli code until a human ran
|
|
8
|
+
* `agents daemon restart`. R5 in the root CLAUDE.md ("An installed CLI and its
|
|
9
|
+
* installed helpers auto-update from the public channel") is binding for the
|
|
10
|
+
* interactive CLI path (`self-update.ts` / `agents upgrade`) but was silently
|
|
11
|
+
* NOT held for the one process that runs unattended for days. This service
|
|
12
|
+
* closes that gap for the daemon specifically, reusing the exact same
|
|
13
|
+
* verified-install primitives `agents upgrade` already uses — it does not
|
|
14
|
+
* fork or reimplement them.
|
|
15
|
+
*
|
|
16
|
+
* Model: verify-then-exit, not swap-in-place. A daemon cannot safely hot-swap
|
|
17
|
+
* its own loaded JS mid-process (in-flight ticks, open sockets, a live
|
|
18
|
+
* ServiceSupervisor). Instead this tick installs + BYTE-VERIFIES the new
|
|
19
|
+
* package on disk, and only once that succeeds does it `process.exit(0)` —
|
|
20
|
+
* the OS supervisor (launchd `KeepAlive` / systemd `Restart=always`, see
|
|
21
|
+
* `daemon/AGENTS.md`'s crash-recovery model) relaunches the daemon, which
|
|
22
|
+
* then boots the new code. Clients do not need to be told anything: browser
|
|
23
|
+
* IPC clients re-probe the socket via `waitForBrowserService`
|
|
24
|
+
* (`browser/ipc.ts`), and the scheduler's atomic `(routine, scheduledFor)`
|
|
25
|
+
* claim (see `docs/specifications.md` §Scheduling & execution singularity)
|
|
26
|
+
* means a routine mid-fire at the moment of exit is deduped safely across the
|
|
27
|
+
* restart rather than double-fired.
|
|
28
|
+
*
|
|
29
|
+
* Fail-closed is the whole point: every step below that can fail — the
|
|
30
|
+
* registry check, the install, the post-install verify — leaves the OLD
|
|
31
|
+
* daemon running untouched and logs a WARN/ERROR for the next tick to retry.
|
|
32
|
+
* The daemon must never exit into code it has not proven is the real,
|
|
33
|
+
* verified, requested version; an unverified exit would let the OS supervisor
|
|
34
|
+
* relaunch-loop on a broken install.
|
|
35
|
+
*/
|
|
36
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
37
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
38
|
+
/**
|
|
39
|
+
* Hard cap per tick: a real download + npm/bun install + verify can
|
|
40
|
+
* legitimately take minutes on a slow link. 15 minutes matches the task's
|
|
41
|
+
* stated budget and is short relative to the ~75min cadence. Exported so the
|
|
42
|
+
* on-demand `request-self-update` IPC handler (`browser/ipc.ts`) can bound
|
|
43
|
+
* its own `AbortController` on the SAME budget the periodic tick runs under —
|
|
44
|
+
* one deadline, not two independently-tuned numbers that could drift apart.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SELF_UPDATE_DEADLINE_MS: number;
|
|
47
|
+
export interface SelfUpdateOutcome {
|
|
48
|
+
updated: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
}
|
|
51
|
+
interface NpmLatestMetadata {
|
|
52
|
+
version: string;
|
|
53
|
+
integrity: string;
|
|
54
|
+
tarball: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Dependency seam so `self-update-service.test.ts` can drive a REAL install
|
|
58
|
+
* against a fixture npm prefix/tarball without touching the real npm
|
|
59
|
+
* registry or the real running install. Production code always uses
|
|
60
|
+
* {@link defaultSelfUpdateDeps} (the default parameter below) — no test-only
|
|
61
|
+
* branch exists in the exported logic itself.
|
|
62
|
+
*/
|
|
63
|
+
export interface SelfUpdateDeps {
|
|
64
|
+
currentVersion(): string;
|
|
65
|
+
isDevBuild(): boolean;
|
|
66
|
+
detectShadow(): boolean;
|
|
67
|
+
packageRoot(): string;
|
|
68
|
+
fetchLatestMetadata(signal: AbortSignal): Promise<NpmLatestMetadata>;
|
|
69
|
+
installAndVerify(metadata: NpmLatestMetadata, packageRoot: string, signal: AbortSignal): Promise<void>;
|
|
70
|
+
syncSystemRepo(): Promise<void>;
|
|
71
|
+
syncLocal(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
export declare function fetchLatestNpmMetadata(signal: AbortSignal): Promise<NpmLatestMetadata>;
|
|
74
|
+
/**
|
|
75
|
+
* Install + byte-verify `metadata` into `packageRoot`'s install, exactly the
|
|
76
|
+
* sequence `bootstrap.ts`'s `installResolvedPackage` runs for `agents
|
|
77
|
+
* upgrade` (download+integrity-verify -> sweep stale staging -> package-
|
|
78
|
+
* manager install -> verify installed version -> refresh alias shims).
|
|
79
|
+
* `bootstrap.ts` cannot be imported here — it runs side-effecting top-level
|
|
80
|
+
* code (argv parsing, command registration) on import, which the daemon must
|
|
81
|
+
* never trigger — so this is the same primitives from `self-update.ts`
|
|
82
|
+
* composed directly, not a fork of the upgrade logic.
|
|
83
|
+
*
|
|
84
|
+
* `signal` is threaded into EVERY step's own `signal` option
|
|
85
|
+
* (`downloadVerifiedTarball` / `installPackageIntoPrefix` /
|
|
86
|
+
* `installPackageWithBun`, all in `self-update.ts`) rather than raced against
|
|
87
|
+
* from the outside: those primitives kill the underlying fetch/child process
|
|
88
|
+
* on abort and their promise rejects only once that real cancellation has
|
|
89
|
+
* happened. A wrapper that merely stopped AWAITING an unkillable operation
|
|
90
|
+
* (the prior approach here) left an orphaned `npm install -g`/`bun add -g`
|
|
91
|
+
* writing into the shared global prefix — which the very next tick's fresh
|
|
92
|
+
* install (started as soon as the supervisor's backoff fires, seconds later)
|
|
93
|
+
* would then race into the same directory. Real cancellation is what makes
|
|
94
|
+
* `attemptSelfUpdateAndExit`'s `inFlightAttempt` dedupe (below) an actual
|
|
95
|
+
* guarantee instead of a guard whose lifetime is shorter than the operation
|
|
96
|
+
* it's guarding (found in review, PHNX-3695).
|
|
97
|
+
*/
|
|
98
|
+
export declare function installAndVerifyDefault(metadata: NpmLatestMetadata, packageRoot: string, signal: AbortSignal): Promise<void>;
|
|
99
|
+
export declare function defaultSelfUpdateDeps(): SelfUpdateDeps;
|
|
100
|
+
/**
|
|
101
|
+
* Core self-update decision + action, shared by the periodic tick
|
|
102
|
+
* ({@link SelfUpdateService.onTick}) and the on-demand IPC path
|
|
103
|
+
* (`request-self-update`, `browser/ipc.ts`) — one implementation, so a
|
|
104
|
+
* version-skew client asking "update now" runs exactly the same fail-closed
|
|
105
|
+
* logic as the scheduled sweep. Returns rather than throws so callers decide
|
|
106
|
+
* their own exit timing (the periodic service exits immediately; the IPC
|
|
107
|
+
* handler must respond to the client on the socket BEFORE exiting, or the
|
|
108
|
+
* client hangs on a socket that is closing mid-write). Concurrent callers
|
|
109
|
+
* share one in-flight attempt rather than racing separate installs.
|
|
110
|
+
*/
|
|
111
|
+
export declare function attemptSelfUpdateAndExit(ctx: DaemonContext, signal: AbortSignal, deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
|
|
112
|
+
/**
|
|
113
|
+
* Schedule the process exit for a verified self-update, exactly once, no
|
|
114
|
+
* matter how many callers observe `outcome.updated` on the shared
|
|
115
|
+
* `inFlightAttempt` promise. The periodic tick and an on-demand
|
|
116
|
+
* `request-self-update` IPC call (`browser/ipc.ts`) can both be awaiting that
|
|
117
|
+
* SAME promise — if the tick's continuation ran an immediate `process.exit(0)`
|
|
118
|
+
* while the IPC handler's continuation had not yet reached `socket.write`,
|
|
119
|
+
* the tick's exit could win the race and the client would see a closed socket
|
|
120
|
+
* before any response (found in review, PHNX-3695). Routing every caller
|
|
121
|
+
* through this one guarded, always-delayed scheduling point means the delay
|
|
122
|
+
* protects EVERY caller's in-flight response, not just the IPC handler's own.
|
|
123
|
+
*/
|
|
124
|
+
export declare function scheduleSelfUpdateExit(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Fire the on-demand self-update in the BACKGROUND and return its (bounded)
|
|
127
|
+
* promise WITHOUT the caller having to await it. This is what keeps the
|
|
128
|
+
* `request-self-update` IPC handler (`browser/ipc.ts`) from parking a
|
|
129
|
+
* version-skewed `agents browser` verb behind the full
|
|
130
|
+
* check→download→install→verify: that handler routes through
|
|
131
|
+
* `reconcileDaemonVersion` on every version-skewed call, so awaiting the whole
|
|
132
|
+
* install there reintroduces exactly the client-stall PHNX-3605 was written to
|
|
133
|
+
* prevent (tens of seconds, worst case ~15 min). The handler instead responds
|
|
134
|
+
* "triggered" immediately and lets this run in the background — the daemon does
|
|
135
|
+
* install→verify→exit(0) on its own, the OS supervisor relaunches it, and the
|
|
136
|
+
* browser reconnects.
|
|
137
|
+
*
|
|
138
|
+
* The work still shares the module-level {@link attemptSelfUpdateAndExit}
|
|
139
|
+
* `inFlightAttempt` guard, so a concurrent trigger (or the periodic tick) can't
|
|
140
|
+
* race a second install into the same prefix. It is bounded by
|
|
141
|
+
* {@link SELF_UPDATE_DEADLINE_MS} via an `AbortController` nobody awaits (the
|
|
142
|
+
* timer is `unref`'d so it never keeps the daemon alive on its own and never
|
|
143
|
+
* dangles in a test). Fail-closed is preserved end to end:
|
|
144
|
+
* `runSelfUpdateAttempt` already turns an install/verify failure into a
|
|
145
|
+
* not-updated outcome that leaves the running daemon untouched. The caller
|
|
146
|
+
* schedules the one decoupled {@link scheduleSelfUpdateExit} off the returned
|
|
147
|
+
* promise once `updated` is true, so the exit still fires after the IPC
|
|
148
|
+
* response has flushed.
|
|
149
|
+
*/
|
|
150
|
+
export declare function triggerSelfUpdateInBackground(ctx: DaemonContext, deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
|
|
151
|
+
/**
|
|
152
|
+
* The subset of self-update decline checks that are INSTANT and network-free —
|
|
153
|
+
* a dev build, or a shadowed install. The on-demand IPC handler
|
|
154
|
+
* (`request-self-update`) runs these SYNCHRONOUSLY so a version-skewed browser
|
|
155
|
+
* client gets the PHNX-3605 "nothing changed, not evicting" advisory
|
|
156
|
+
* immediately, instead of a "triggered" it would never act on. The remaining
|
|
157
|
+
* checks (registry probe, already-current, install/verify) stay inside the
|
|
158
|
+
* backgrounded {@link attemptSelfUpdateAndExit} so the handler never blocks on
|
|
159
|
+
* the network or the install. This is the single source of the two instant
|
|
160
|
+
* decline reasons — {@link runSelfUpdateAttempt} calls it too, so the on-demand
|
|
161
|
+
* decline text can never drift from the periodic tick's. Returns the decline
|
|
162
|
+
* reason, or `null` to proceed to the (backgrounded) install path.
|
|
163
|
+
*/
|
|
164
|
+
export declare function selfUpdateSyncDeclineReason(deps?: SelfUpdateDeps): string | null;
|
|
165
|
+
export declare class SelfUpdateService extends BasePeriodicService {
|
|
166
|
+
readonly id: DaemonServiceId;
|
|
167
|
+
readonly intervalMs: number;
|
|
168
|
+
readonly deadlineMs: number;
|
|
169
|
+
readonly startupDelayMs: number;
|
|
170
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
171
|
+
protected onStop(): Promise<void>;
|
|
172
|
+
protected onTick(ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
export {};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon self-update service (PHNX-3695, "Fix 2").
|
|
3
|
+
*
|
|
4
|
+
* The daemon (`agents __daemon-run`) is a long-running background process that
|
|
5
|
+
* historically opted OUT of the CLI's interactive auto-update: `bootstrap.ts`
|
|
6
|
+
* force-sets `AGENTS_CLI_DISABLE_AUTO_UPDATE=1` for `__daemon-run`, so a
|
|
7
|
+
* running daemon never picked up new agents-cli code until a human ran
|
|
8
|
+
* `agents daemon restart`. R5 in the root CLAUDE.md ("An installed CLI and its
|
|
9
|
+
* installed helpers auto-update from the public channel") is binding for the
|
|
10
|
+
* interactive CLI path (`self-update.ts` / `agents upgrade`) but was silently
|
|
11
|
+
* NOT held for the one process that runs unattended for days. This service
|
|
12
|
+
* closes that gap for the daemon specifically, reusing the exact same
|
|
13
|
+
* verified-install primitives `agents upgrade` already uses — it does not
|
|
14
|
+
* fork or reimplement them.
|
|
15
|
+
*
|
|
16
|
+
* Model: verify-then-exit, not swap-in-place. A daemon cannot safely hot-swap
|
|
17
|
+
* its own loaded JS mid-process (in-flight ticks, open sockets, a live
|
|
18
|
+
* ServiceSupervisor). Instead this tick installs + BYTE-VERIFIES the new
|
|
19
|
+
* package on disk, and only once that succeeds does it `process.exit(0)` —
|
|
20
|
+
* the OS supervisor (launchd `KeepAlive` / systemd `Restart=always`, see
|
|
21
|
+
* `daemon/AGENTS.md`'s crash-recovery model) relaunches the daemon, which
|
|
22
|
+
* then boots the new code. Clients do not need to be told anything: browser
|
|
23
|
+
* IPC clients re-probe the socket via `waitForBrowserService`
|
|
24
|
+
* (`browser/ipc.ts`), and the scheduler's atomic `(routine, scheduledFor)`
|
|
25
|
+
* claim (see `docs/specifications.md` §Scheduling & execution singularity)
|
|
26
|
+
* means a routine mid-fire at the moment of exit is deduped safely across the
|
|
27
|
+
* restart rather than double-fired.
|
|
28
|
+
*
|
|
29
|
+
* Fail-closed is the whole point: every step below that can fail — the
|
|
30
|
+
* registry check, the install, the post-install verify — leaves the OLD
|
|
31
|
+
* daemon running untouched and logs a WARN/ERROR for the next tick to retry.
|
|
32
|
+
* The daemon must never exit into code it has not proven is the real,
|
|
33
|
+
* verified, requested version; an unverified exit would let the OS supervisor
|
|
34
|
+
* relaunch-loop on a broken install.
|
|
35
|
+
*/
|
|
36
|
+
import { BasePeriodicService } from './service.js';
|
|
37
|
+
import { getCliVersion } from '../version.js';
|
|
38
|
+
import { isDevVersionStamp } from '../startup/dev-build.js';
|
|
39
|
+
import { detectAgentsBinaryShadows } from '../binary-shadow.js';
|
|
40
|
+
import { compareVersions } from '../agent-spec/primitives.js';
|
|
41
|
+
import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, downloadVerifiedTarball, ensureGlobalBinLinks, installPackageIntoPrefix, installPackageWithBun, refreshAliasShims, resolveRunningPackageRoot, sweepStaleInstallStaging, verifyInstalledVersion, } from '../self-update.js';
|
|
42
|
+
import { tryAutoPullSystemRepo } from '../git.js';
|
|
43
|
+
import { getSystemAgentsDir } from '../state.js';
|
|
44
|
+
import { runUmbrellaSync } from '../sync-umbrella.js';
|
|
45
|
+
import * as fs from 'fs';
|
|
46
|
+
import * as path from 'path';
|
|
47
|
+
import { fileURLToPath } from 'url';
|
|
48
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
/** Runs roughly every 75 minutes — self-update is not urgent (unlike self-heal's 6h drift repair, it changes running code, so it stays well under a day but still infrequent). */
|
|
50
|
+
const SELF_UPDATE_TICK_MS = 75 * 60_000;
|
|
51
|
+
/**
|
|
52
|
+
* Hard cap per tick: a real download + npm/bun install + verify can
|
|
53
|
+
* legitimately take minutes on a slow link. 15 minutes matches the task's
|
|
54
|
+
* stated budget and is short relative to the ~75min cadence. Exported so the
|
|
55
|
+
* on-demand `request-self-update` IPC handler (`browser/ipc.ts`) can bound
|
|
56
|
+
* its own `AbortController` on the SAME budget the periodic tick runs under —
|
|
57
|
+
* one deadline, not two independently-tuned numbers that could drift apart.
|
|
58
|
+
*/
|
|
59
|
+
export const SELF_UPDATE_DEADLINE_MS = 15 * 60_000;
|
|
60
|
+
/**
|
|
61
|
+
* First tick fires 5 minutes after daemon boot — deliberately longer than
|
|
62
|
+
* self-heal's 30s stagger (`self-heal-service.ts`): self-heal repairs local
|
|
63
|
+
* drift and is cheap/safe to run immediately, while self-update can replace
|
|
64
|
+
* the running package and exit the process, which should never be the very
|
|
65
|
+
* first thing a freshly-started daemon does (give the box a moment to finish
|
|
66
|
+
* settling — shims, PATH, other services' startup ticks — before considering
|
|
67
|
+
* a restart). Every later tick still fires on the normal cadence.
|
|
68
|
+
*/
|
|
69
|
+
const SELF_UPDATE_STARTUP_DELAY_MS = 5 * 60_000;
|
|
70
|
+
export async function fetchLatestNpmMetadata(signal) {
|
|
71
|
+
const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`, { signal });
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`registry.npmjs.org responded ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
if (typeof data.version !== 'string'
|
|
77
|
+
|| typeof data.dist?.integrity !== 'string'
|
|
78
|
+
|| typeof data.dist?.tarball !== 'string') {
|
|
79
|
+
throw new Error('npm registry response did not include version, integrity, and tarball');
|
|
80
|
+
}
|
|
81
|
+
return { version: data.version, integrity: data.dist.integrity, tarball: data.dist.tarball };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Install + byte-verify `metadata` into `packageRoot`'s install, exactly the
|
|
85
|
+
* sequence `bootstrap.ts`'s `installResolvedPackage` runs for `agents
|
|
86
|
+
* upgrade` (download+integrity-verify -> sweep stale staging -> package-
|
|
87
|
+
* manager install -> verify installed version -> refresh alias shims).
|
|
88
|
+
* `bootstrap.ts` cannot be imported here — it runs side-effecting top-level
|
|
89
|
+
* code (argv parsing, command registration) on import, which the daemon must
|
|
90
|
+
* never trigger — so this is the same primitives from `self-update.ts`
|
|
91
|
+
* composed directly, not a fork of the upgrade logic.
|
|
92
|
+
*
|
|
93
|
+
* `signal` is threaded into EVERY step's own `signal` option
|
|
94
|
+
* (`downloadVerifiedTarball` / `installPackageIntoPrefix` /
|
|
95
|
+
* `installPackageWithBun`, all in `self-update.ts`) rather than raced against
|
|
96
|
+
* from the outside: those primitives kill the underlying fetch/child process
|
|
97
|
+
* on abort and their promise rejects only once that real cancellation has
|
|
98
|
+
* happened. A wrapper that merely stopped AWAITING an unkillable operation
|
|
99
|
+
* (the prior approach here) left an orphaned `npm install -g`/`bun add -g`
|
|
100
|
+
* writing into the shared global prefix — which the very next tick's fresh
|
|
101
|
+
* install (started as soon as the supervisor's backoff fires, seconds later)
|
|
102
|
+
* would then race into the same directory. Real cancellation is what makes
|
|
103
|
+
* `attemptSelfUpdateAndExit`'s `inFlightAttempt` dedupe (below) an actual
|
|
104
|
+
* guarantee instead of a guard whose lifetime is shorter than the operation
|
|
105
|
+
* it's guarding (found in review, PHNX-3695).
|
|
106
|
+
*/
|
|
107
|
+
export async function installAndVerifyDefault(metadata, packageRoot, signal) {
|
|
108
|
+
const tarball = await downloadVerifiedTarball(metadata.tarball, metadata.integrity, 60_000, signal);
|
|
109
|
+
try {
|
|
110
|
+
await sweepStaleInstallStaging(packageRoot);
|
|
111
|
+
if (detectPackageManager(packageRoot) === 'bun') {
|
|
112
|
+
await installPackageWithBun(tarball, signal);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
await installPackageIntoPrefix(tarball, deriveGlobalPrefix(packageRoot), signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
try {
|
|
120
|
+
await fs.promises.rm(path.dirname(tarball), { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* leave it for the OS temp sweep */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await verifyInstalledVersion(packageRoot, metadata.version);
|
|
127
|
+
await refreshAliasShims(packageRoot, signal);
|
|
128
|
+
// PHNX-2768: mirror `bootstrap.ts`'s `installResolvedPackage` — an
|
|
129
|
+
// `--ignore-scripts` install (both package-manager paths above) can leave
|
|
130
|
+
// the package.json at the new version but the global bin links
|
|
131
|
+
// (agents/ag/browser/computer) GONE. Without this, a self-update tick could
|
|
132
|
+
// exit `updated: true` while every operator-typed `agents` command on that
|
|
133
|
+
// box now reads "command not found," with no signal anywhere pointing at
|
|
134
|
+
// why. Same fail-loud contract as the interactive path: a link that cannot
|
|
135
|
+
// be made to resolve fails the whole attempt rather than reporting success.
|
|
136
|
+
if (detectPackageManager(packageRoot) !== 'bun' && process.platform !== 'win32') {
|
|
137
|
+
const prefix = deriveGlobalPrefix(packageRoot);
|
|
138
|
+
const repairs = await ensureGlobalBinLinks(packageRoot, prefix);
|
|
139
|
+
const failed = repairs.filter((r) => r.action === 'failed');
|
|
140
|
+
if (failed.length > 0) {
|
|
141
|
+
const relink = failed
|
|
142
|
+
.map((r) => `ln -sf ${path.relative(path.dirname(r.linkPath), r.target)} ${r.linkPath}`)
|
|
143
|
+
.join(' && ');
|
|
144
|
+
throw new Error(`upgraded to ${metadata.version} but could not restore the ` +
|
|
145
|
+
`${failed.map((r) => r.name).join(', ')} command link${failed.length === 1 ? '' : 's'} in ` +
|
|
146
|
+
`${path.join(prefix, 'bin')} (${failed.map((r) => r.error).join('; ')}). ` +
|
|
147
|
+
`The box has the new package but no working \`agents\` — relink manually: ${relink}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function defaultSelfUpdateDeps() {
|
|
152
|
+
return {
|
|
153
|
+
currentVersion: () => getCliVersion(),
|
|
154
|
+
isDevBuild: () => isDevVersionStamp(getCliVersion()),
|
|
155
|
+
detectShadow: () => detectAgentsBinaryShadows().length > 0,
|
|
156
|
+
packageRoot: () => resolveRunningPackageRoot(__dirname),
|
|
157
|
+
fetchLatestMetadata: fetchLatestNpmMetadata,
|
|
158
|
+
installAndVerify: installAndVerifyDefault,
|
|
159
|
+
syncSystemRepo: async () => {
|
|
160
|
+
const result = await tryAutoPullSystemRepo(getSystemAgentsDir());
|
|
161
|
+
if (result.refused) {
|
|
162
|
+
throw new Error(`system repo origin '${result.actualRemote}' is not the expected system remote — refused`);
|
|
163
|
+
}
|
|
164
|
+
if (result.error) {
|
|
165
|
+
throw new Error(result.error);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
syncLocal: async () => {
|
|
169
|
+
// Reconcile-only (no fetch — the .system pull above already fetched);
|
|
170
|
+
// best-effort, same as the system-repo pull: a declined resource here
|
|
171
|
+
// is not a self-update failure.
|
|
172
|
+
await runUmbrellaSync({
|
|
173
|
+
flags: { local: true },
|
|
174
|
+
log: () => { },
|
|
175
|
+
yes: true,
|
|
176
|
+
quiet: true,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Dedupes concurrent callers onto ONE in-flight attempt. The periodic tick
|
|
183
|
+
* and an on-demand `request-self-update` IPC call (possibly several, if more
|
|
184
|
+
* than one version-skewed client reconnects at once) can overlap in the same
|
|
185
|
+
* process — without this, two concurrent `installAndVerify` calls race on the
|
|
186
|
+
* same package-manager install directory. Keyed process-wide (not per-deps)
|
|
187
|
+
* since production always shares one `defaultSelfUpdateDeps()` install target;
|
|
188
|
+
* tests inject distinct `deps` per case and don't run concurrently with each
|
|
189
|
+
* other, so this never cross-contaminates test outcomes.
|
|
190
|
+
*/
|
|
191
|
+
let inFlightAttempt = null;
|
|
192
|
+
/**
|
|
193
|
+
* Core self-update decision + action, shared by the periodic tick
|
|
194
|
+
* ({@link SelfUpdateService.onTick}) and the on-demand IPC path
|
|
195
|
+
* (`request-self-update`, `browser/ipc.ts`) — one implementation, so a
|
|
196
|
+
* version-skew client asking "update now" runs exactly the same fail-closed
|
|
197
|
+
* logic as the scheduled sweep. Returns rather than throws so callers decide
|
|
198
|
+
* their own exit timing (the periodic service exits immediately; the IPC
|
|
199
|
+
* handler must respond to the client on the socket BEFORE exiting, or the
|
|
200
|
+
* client hangs on a socket that is closing mid-write). Concurrent callers
|
|
201
|
+
* share one in-flight attempt rather than racing separate installs.
|
|
202
|
+
*/
|
|
203
|
+
export async function attemptSelfUpdateAndExit(ctx, signal, deps = defaultSelfUpdateDeps()) {
|
|
204
|
+
if (inFlightAttempt)
|
|
205
|
+
return inFlightAttempt;
|
|
206
|
+
const attempt = runSelfUpdateAttempt(ctx, signal, deps);
|
|
207
|
+
inFlightAttempt = attempt;
|
|
208
|
+
try {
|
|
209
|
+
return await attempt;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
if (inFlightAttempt === attempt)
|
|
213
|
+
inFlightAttempt = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function runSelfUpdateAttempt(ctx, signal, deps) {
|
|
217
|
+
const syncDecline = selfUpdateSyncDeclineReason(deps);
|
|
218
|
+
if (syncDecline)
|
|
219
|
+
return { updated: false, reason: syncDecline };
|
|
220
|
+
const current = deps.currentVersion();
|
|
221
|
+
let metadata;
|
|
222
|
+
try {
|
|
223
|
+
metadata = await deps.fetchLatestMetadata(signal);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
ctx.log('WARN', `self-update: registry check failed, staying on ${current}: ${message}`);
|
|
228
|
+
return { updated: false, reason: 'registry check failed' };
|
|
229
|
+
}
|
|
230
|
+
if (compareVersions(metadata.version, current) <= 0) {
|
|
231
|
+
return { updated: false, reason: `already current (${current})` };
|
|
232
|
+
}
|
|
233
|
+
const packageRoot = deps.packageRoot();
|
|
234
|
+
try {
|
|
235
|
+
await deps.installAndVerify(metadata, packageRoot, signal);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
239
|
+
ctx.log('ERROR', `self-update: install/verify of ${metadata.version} failed, staying on ${current}: ${message}`);
|
|
240
|
+
return { updated: false, reason: 'install or verify failed' };
|
|
241
|
+
}
|
|
242
|
+
// Best-effort from here: the CLI package itself is already installed and
|
|
243
|
+
// byte-verified, so a failure pulling the companion .system repo or
|
|
244
|
+
// reconciling resources must not undo a good CLI upgrade or block the exit
|
|
245
|
+
// that lets the OS supervisor relaunch onto it — it is logged and left for
|
|
246
|
+
// the NEXT tick (which runs on the new code) to retry.
|
|
247
|
+
try {
|
|
248
|
+
await deps.syncSystemRepo();
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
252
|
+
ctx.log('WARN', `self-update: .system repo pull failed: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await deps.syncLocal();
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
259
|
+
ctx.log('WARN', `self-update: local reconcile ('agents sync --local') failed: ${message}`);
|
|
260
|
+
}
|
|
261
|
+
ctx.log('INFO', `self-update: verified ${current} -> ${metadata.version}; exiting for OS-supervisor relaunch`);
|
|
262
|
+
return { updated: true };
|
|
263
|
+
}
|
|
264
|
+
/** How long `scheduleSelfUpdateExit` waits before `process.exit(0)` — long enough for an IPC handler's `socket.write` to flush to the OS. */
|
|
265
|
+
const SELF_UPDATE_EXIT_DELAY_MS = 250;
|
|
266
|
+
let exitScheduled = false;
|
|
267
|
+
/**
|
|
268
|
+
* Schedule the process exit for a verified self-update, exactly once, no
|
|
269
|
+
* matter how many callers observe `outcome.updated` on the shared
|
|
270
|
+
* `inFlightAttempt` promise. The periodic tick and an on-demand
|
|
271
|
+
* `request-self-update` IPC call (`browser/ipc.ts`) can both be awaiting that
|
|
272
|
+
* SAME promise — if the tick's continuation ran an immediate `process.exit(0)`
|
|
273
|
+
* while the IPC handler's continuation had not yet reached `socket.write`,
|
|
274
|
+
* the tick's exit could win the race and the client would see a closed socket
|
|
275
|
+
* before any response (found in review, PHNX-3695). Routing every caller
|
|
276
|
+
* through this one guarded, always-delayed scheduling point means the delay
|
|
277
|
+
* protects EVERY caller's in-flight response, not just the IPC handler's own.
|
|
278
|
+
*/
|
|
279
|
+
export function scheduleSelfUpdateExit() {
|
|
280
|
+
if (exitScheduled)
|
|
281
|
+
return;
|
|
282
|
+
exitScheduled = true;
|
|
283
|
+
setTimeout(() => process.exit(0), SELF_UPDATE_EXIT_DELAY_MS);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Fire the on-demand self-update in the BACKGROUND and return its (bounded)
|
|
287
|
+
* promise WITHOUT the caller having to await it. This is what keeps the
|
|
288
|
+
* `request-self-update` IPC handler (`browser/ipc.ts`) from parking a
|
|
289
|
+
* version-skewed `agents browser` verb behind the full
|
|
290
|
+
* check→download→install→verify: that handler routes through
|
|
291
|
+
* `reconcileDaemonVersion` on every version-skewed call, so awaiting the whole
|
|
292
|
+
* install there reintroduces exactly the client-stall PHNX-3605 was written to
|
|
293
|
+
* prevent (tens of seconds, worst case ~15 min). The handler instead responds
|
|
294
|
+
* "triggered" immediately and lets this run in the background — the daemon does
|
|
295
|
+
* install→verify→exit(0) on its own, the OS supervisor relaunches it, and the
|
|
296
|
+
* browser reconnects.
|
|
297
|
+
*
|
|
298
|
+
* The work still shares the module-level {@link attemptSelfUpdateAndExit}
|
|
299
|
+
* `inFlightAttempt` guard, so a concurrent trigger (or the periodic tick) can't
|
|
300
|
+
* race a second install into the same prefix. It is bounded by
|
|
301
|
+
* {@link SELF_UPDATE_DEADLINE_MS} via an `AbortController` nobody awaits (the
|
|
302
|
+
* timer is `unref`'d so it never keeps the daemon alive on its own and never
|
|
303
|
+
* dangles in a test). Fail-closed is preserved end to end:
|
|
304
|
+
* `runSelfUpdateAttempt` already turns an install/verify failure into a
|
|
305
|
+
* not-updated outcome that leaves the running daemon untouched. The caller
|
|
306
|
+
* schedules the one decoupled {@link scheduleSelfUpdateExit} off the returned
|
|
307
|
+
* promise once `updated` is true, so the exit still fires after the IPC
|
|
308
|
+
* response has flushed.
|
|
309
|
+
*/
|
|
310
|
+
export function triggerSelfUpdateInBackground(ctx, deps = defaultSelfUpdateDeps()) {
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
const timeout = setTimeout(() => controller.abort(), SELF_UPDATE_DEADLINE_MS);
|
|
313
|
+
if (typeof timeout.unref === 'function')
|
|
314
|
+
timeout.unref();
|
|
315
|
+
return attemptSelfUpdateAndExit(ctx, controller.signal, deps).finally(() => clearTimeout(timeout));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The subset of self-update decline checks that are INSTANT and network-free —
|
|
319
|
+
* a dev build, or a shadowed install. The on-demand IPC handler
|
|
320
|
+
* (`request-self-update`) runs these SYNCHRONOUSLY so a version-skewed browser
|
|
321
|
+
* client gets the PHNX-3605 "nothing changed, not evicting" advisory
|
|
322
|
+
* immediately, instead of a "triggered" it would never act on. The remaining
|
|
323
|
+
* checks (registry probe, already-current, install/verify) stay inside the
|
|
324
|
+
* backgrounded {@link attemptSelfUpdateAndExit} so the handler never blocks on
|
|
325
|
+
* the network or the install. This is the single source of the two instant
|
|
326
|
+
* decline reasons — {@link runSelfUpdateAttempt} calls it too, so the on-demand
|
|
327
|
+
* decline text can never drift from the periodic tick's. Returns the decline
|
|
328
|
+
* reason, or `null` to proceed to the (backgrounded) install path.
|
|
329
|
+
*/
|
|
330
|
+
export function selfUpdateSyncDeclineReason(deps = defaultSelfUpdateDeps()) {
|
|
331
|
+
if (deps.isDevBuild())
|
|
332
|
+
return 'dev build — self-update is a no-op';
|
|
333
|
+
if (deps.detectShadow())
|
|
334
|
+
return 'another agents binary shadows this install — self-update is a no-op';
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
export class SelfUpdateService extends BasePeriodicService {
|
|
338
|
+
id = 'self-update';
|
|
339
|
+
intervalMs = SELF_UPDATE_TICK_MS;
|
|
340
|
+
deadlineMs = SELF_UPDATE_DEADLINE_MS;
|
|
341
|
+
startupDelayMs = SELF_UPDATE_STARTUP_DELAY_MS;
|
|
342
|
+
async onStart(_ctx) {
|
|
343
|
+
// No connections/handles to open — each tick checks the registry fresh.
|
|
344
|
+
}
|
|
345
|
+
async onStop() {
|
|
346
|
+
// Nothing to release — the supervisor's timer teardown is the only cleanup needed.
|
|
347
|
+
}
|
|
348
|
+
async onTick(ctx, signal) {
|
|
349
|
+
const outcome = await attemptSelfUpdateAndExit(ctx, signal);
|
|
350
|
+
if (outcome.updated)
|
|
351
|
+
scheduleSelfUpdateExit();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
* `supervisor.start()` pairing this requires.
|
|
24
24
|
*/
|
|
25
25
|
import { BasePeriodicService } from './service.js';
|
|
26
|
-
import * as
|
|
26
|
+
import * as fsp from 'fs/promises';
|
|
27
27
|
/** Matches the historical inline interval (daemon.ts STATE_DIR_CHECK_TICK_MS), overridable for tests. */
|
|
28
28
|
const STATE_DIR_CHECK_TICK_MS = 60_000;
|
|
29
|
-
/** Hard cap per tick — a single
|
|
29
|
+
/** Hard cap per tick — a single async file read, far above what it could ever need. */
|
|
30
30
|
const STATE_DIR_CHECK_DEADLINE_MS = 5_000;
|
|
31
31
|
export class StateDirCheckService extends BasePeriodicService {
|
|
32
32
|
id = 'state-dir-check';
|
|
@@ -50,7 +50,7 @@ export class StateDirCheckService extends BasePeriodicService {
|
|
|
50
50
|
async onTick(ctx) {
|
|
51
51
|
let markerMatches = false;
|
|
52
52
|
try {
|
|
53
|
-
markerMatches =
|
|
53
|
+
markerMatches = (await fsp.readFile(this.lifetimePath, 'utf-8')) === this.lifetimeToken;
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
56
56
|
// A missing state dir or marker is the condition this guard detects.
|
|
@@ -23,7 +23,7 @@ export class UsageSyncService extends BasePeriodicService {
|
|
|
23
23
|
}
|
|
24
24
|
async onTick(ctx) {
|
|
25
25
|
const { consumeUsageSnapshotsFromSharedStore, publishUsageSnapshotToSharedStore } = await import('../accounting/usage-sync.js');
|
|
26
|
-
const published = publishUsageSnapshotToSharedStore();
|
|
26
|
+
const published = await publishUsageSnapshotToSharedStore();
|
|
27
27
|
if (published.changed)
|
|
28
28
|
ctx.log('INFO', `usage-sync: published usage snapshot to ${published.path}`);
|
|
29
29
|
if (published.error)
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* inline behavior (daemon.ts previously `WATCHDOG_TICK_MS`-interval closure).
|
|
9
9
|
*/
|
|
10
10
|
import { BasePeriodicService } from './service.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { getConfigValueAsync } from '../device-config.js';
|
|
12
|
+
import { emitAsync } from '../feed/events.js';
|
|
13
13
|
/** Matches the historical inline interval (daemon.ts WATCHDOG_TICK_MS). */
|
|
14
14
|
const WATCHDOG_TICK_MS = 3 * 60_000;
|
|
15
15
|
/** Hard cap per tick — `runWatchdogPass` is host-local (adds no SSH fan-out of its own, `watchdog/runner.ts:580`); short enough that a hang never freezes the service for long. */
|
|
@@ -25,12 +25,12 @@ export class WatchdogService extends BasePeriodicService {
|
|
|
25
25
|
// Nothing to release — the supervisor's timer teardown is the only cleanup needed.
|
|
26
26
|
}
|
|
27
27
|
async onTick(ctx) {
|
|
28
|
-
if (
|
|
28
|
+
if ((await getConfigValueAsync('watchdog.enabled')).value !== true)
|
|
29
29
|
return;
|
|
30
30
|
const { runWatchdogPass } = await import('../watchdog/service.js');
|
|
31
31
|
const result = await runWatchdogPass({ nudge: true });
|
|
32
32
|
ctx.log('INFO', `watchdog: ${result.counts.total} live, ${result.counts.stalled} stalled, ${result.counts.nudged} nudged`);
|
|
33
|
-
|
|
33
|
+
await emitAsync('watchdog.action', {
|
|
34
34
|
module: 'watchdog',
|
|
35
35
|
total: result.counts.total,
|
|
36
36
|
stalled: result.counts.stalled,
|
|
@@ -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 = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | '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';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'keychain-reap' | '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';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -47,6 +47,11 @@ export const DAEMON_SERVICES = [
|
|
|
47
47
|
title: 'Self-heal registry',
|
|
48
48
|
description: 'Repairs shims, PATH, shadowing, and resource drift on a schedule.',
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
id: 'self-update',
|
|
52
|
+
title: 'Self-update',
|
|
53
|
+
description: 'Checks npm for a newer agents-cli, installs + verifies it, then exits so the OS supervisor relaunches onto the new code (PHNX-3695).',
|
|
54
|
+
},
|
|
50
55
|
{
|
|
51
56
|
id: 'keychain-reap',
|
|
52
57
|
title: 'Keychain reap',
|