@phnx-labs/agents-cli 1.22.65 → 1.22.68
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 +39 -0
- package/README.md +7 -2
- package/dist/bootstrap.js +3 -3
- package/dist/commands/apply.js +9 -2
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +40 -0
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +33 -9
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +11 -3
- package/dist/lib/accounting/usage.js +23 -10
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/known-hosts.d.ts +15 -0
- package/dist/lib/devices/known-hosts.js +18 -0
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +9 -2
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +13 -0
- package/dist/lib/run-trace-sync.js +77 -0
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +120 -102
- package/dist/lib/session/active.d.ts +3 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +5 -2
- package/dist/lib/session/db.js +54 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +106 -25
- package/dist/lib/session/prompt.d.ts +11 -0
- package/dist/lib/session/prompt.js +25 -0
- package/dist/lib/session/recovery.d.ts +30 -2
- package/dist/lib/session/recovery.js +56 -4
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +185 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/publish.d.ts +55 -9
- package/dist/lib/share/publish.js +87 -9
- package/dist/lib/share/worker-template.js +100 -8
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/package.json +1 -1
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Account-state
|
|
2
|
+
* Account-state services as supervised `PeriodicService`s (PHNX-3608).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The daemon owns usage and authentication health as first-party device state.
|
|
5
|
+
* This used to run its OWN two `setInterval` loops behind a `usageRunning` /
|
|
6
|
+
* `authRunning` latch with NO deadline (`account-state-service.ts`, now removed):
|
|
7
|
+
* a `runUsageRefreshTick` that hung on an unbounded provider await latched
|
|
8
|
+
* `usageRunning = true` forever, so the usage cache froze for the daemon's whole
|
|
9
|
+
* life while the service still looked healthy — the "12h usage-dark" root cause.
|
|
10
|
+
*
|
|
11
|
+
* Now usage and auth are TWO independent supervised services, each with its own
|
|
12
|
+
* timer, per-tick deadline, AbortSignal, and — crucially — its own circuit
|
|
13
|
+
* breaker (`AccountUsageService` = `account-state`, `AccountAuthService` =
|
|
14
|
+
* `account-auth`). Keeping them separate means a run of usage-refresh failures
|
|
15
|
+
* parks ONLY usage and never starves the slower auth refresh (they hit different
|
|
16
|
+
* endpoints), matching the old design's independent loops. Each threads its
|
|
17
|
+
* deadline AbortSignal into its refresh so the tick unwinds at the deadline
|
|
18
|
+
* instead of blocking; the provider fetch is itself already bounded
|
|
19
|
+
* (`AbortSignal.timeout` at the leaf), and the supervisor abandons + restarts a
|
|
20
|
+
* hung tick regardless.
|
|
8
21
|
*/
|
|
9
|
-
import {
|
|
22
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
10
23
|
import type { DaemonServiceId } from '../daemon-services.js';
|
|
11
|
-
|
|
24
|
+
/** Usage cache refresh cadence. */
|
|
25
|
+
export declare const USAGE_STATE_TICK_MS = 60000;
|
|
26
|
+
/** Fleet-auth refresh cadence — deliberately slower (rate-limited endpoint). */
|
|
27
|
+
export declare const AUTH_STATE_TICK_MS: number;
|
|
28
|
+
/**
|
|
29
|
+
* Refresh the usage cache the `agents run` router reads. Independent circuit
|
|
30
|
+
* breaker from auth — a persistently-failing usage endpoint parks only this.
|
|
31
|
+
*/
|
|
32
|
+
export declare class AccountUsageService extends BasePeriodicService {
|
|
33
|
+
readonly id: DaemonServiceId;
|
|
34
|
+
readonly intervalMs = 60000;
|
|
35
|
+
readonly deadlineMs: number;
|
|
36
|
+
private readonly refresh;
|
|
37
|
+
constructor(refresh?: (signal: AbortSignal) => Promise<void>);
|
|
38
|
+
protected onStart(): Promise<void>;
|
|
39
|
+
protected onStop(): Promise<void>;
|
|
40
|
+
protected onTick(_ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Publish this host's fleet-status row and refresh auth health. Independent
|
|
44
|
+
* circuit breaker from usage, and on a slower cadence (the auth verdict rides a
|
|
45
|
+
* rate-limited endpoint).
|
|
46
|
+
*/
|
|
47
|
+
export declare class AccountAuthService extends BasePeriodicService {
|
|
12
48
|
readonly id: DaemonServiceId;
|
|
13
|
-
|
|
14
|
-
|
|
49
|
+
readonly intervalMs: number;
|
|
50
|
+
readonly deadlineMs: number;
|
|
51
|
+
private readonly refresh;
|
|
52
|
+
constructor(refresh?: (signal: AbortSignal) => Promise<void>);
|
|
53
|
+
protected onStart(): Promise<void>;
|
|
15
54
|
protected onStop(): Promise<void>;
|
|
55
|
+
protected onTick(_ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
16
56
|
}
|
|
@@ -1,26 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Account-state
|
|
2
|
+
* Account-state services as supervised `PeriodicService`s (PHNX-3608).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The daemon owns usage and authentication health as first-party device state.
|
|
5
|
+
* This used to run its OWN two `setInterval` loops behind a `usageRunning` /
|
|
6
|
+
* `authRunning` latch with NO deadline (`account-state-service.ts`, now removed):
|
|
7
|
+
* a `runUsageRefreshTick` that hung on an unbounded provider await latched
|
|
8
|
+
* `usageRunning = true` forever, so the usage cache froze for the daemon's whole
|
|
9
|
+
* life while the service still looked healthy — the "12h usage-dark" root cause.
|
|
10
|
+
*
|
|
11
|
+
* Now usage and auth are TWO independent supervised services, each with its own
|
|
12
|
+
* timer, per-tick deadline, AbortSignal, and — crucially — its own circuit
|
|
13
|
+
* breaker (`AccountUsageService` = `account-state`, `AccountAuthService` =
|
|
14
|
+
* `account-auth`). Keeping them separate means a run of usage-refresh failures
|
|
15
|
+
* parks ONLY usage and never starves the slower auth refresh (they hit different
|
|
16
|
+
* endpoints), matching the old design's independent loops. Each threads its
|
|
17
|
+
* deadline AbortSignal into its refresh so the tick unwinds at the deadline
|
|
18
|
+
* instead of blocking; the provider fetch is itself already bounded
|
|
19
|
+
* (`AbortSignal.timeout` at the leaf), and the supervisor abandons + restarts a
|
|
20
|
+
* hung tick regardless.
|
|
8
21
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import { startAccountStateService } from '../account-state-service.js';
|
|
22
|
+
import { BasePeriodicService } from './service.js';
|
|
11
23
|
import { runUsageRefreshTick, runFleetCacheWarmTick } from '../daemon-ticks.js';
|
|
12
|
-
|
|
24
|
+
/** Usage cache refresh cadence. */
|
|
25
|
+
export const USAGE_STATE_TICK_MS = 60_000;
|
|
26
|
+
/** Fleet-auth refresh cadence — deliberately slower (rate-limited endpoint). */
|
|
27
|
+
export const AUTH_STATE_TICK_MS = 3 * 60_000;
|
|
28
|
+
/** A hung refresh is abandoned at this deadline; a real sweep is far under it. */
|
|
29
|
+
const REFRESH_DEADLINE_MS = 2 * 60_000;
|
|
30
|
+
/**
|
|
31
|
+
* A promise that rejects when `signal` aborts (immediately if already aborted),
|
|
32
|
+
* so a tick can `Promise.race` its work against the supervisor's deadline and
|
|
33
|
+
* unwind instead of blocking on an await that may never settle. The signal is
|
|
34
|
+
* per-tick, so the once-listener is dropped with it — no cross-tick leak.
|
|
35
|
+
*/
|
|
36
|
+
function abortRejection(signal, label) {
|
|
37
|
+
return new Promise((_, reject) => {
|
|
38
|
+
if (signal.aborted) {
|
|
39
|
+
reject(new Error(label));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
signal.addEventListener('abort', () => reject(new Error(label)), { once: true });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Refresh the usage cache the `agents run` router reads. Independent circuit
|
|
47
|
+
* breaker from auth — a persistently-failing usage endpoint parks only this.
|
|
48
|
+
*/
|
|
49
|
+
export class AccountUsageService extends BasePeriodicService {
|
|
13
50
|
id = 'account-state';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
51
|
+
intervalMs = USAGE_STATE_TICK_MS;
|
|
52
|
+
deadlineMs = REFRESH_DEADLINE_MS;
|
|
53
|
+
refresh;
|
|
54
|
+
constructor(refresh = runUsageRefreshTick) {
|
|
55
|
+
super();
|
|
56
|
+
this.refresh = refresh;
|
|
57
|
+
}
|
|
58
|
+
async onStart() { }
|
|
59
|
+
async onStop() { }
|
|
60
|
+
async onTick(_ctx, signal) {
|
|
61
|
+
await Promise.race([
|
|
62
|
+
this.refresh(signal),
|
|
63
|
+
abortRejection(signal, 'usage refresh aborted at deadline'),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Publish this host's fleet-status row and refresh auth health. Independent
|
|
69
|
+
* circuit breaker from usage, and on a slower cadence (the auth verdict rides a
|
|
70
|
+
* rate-limited endpoint).
|
|
71
|
+
*/
|
|
72
|
+
export class AccountAuthService extends BasePeriodicService {
|
|
73
|
+
id = 'account-auth';
|
|
74
|
+
intervalMs = AUTH_STATE_TICK_MS;
|
|
75
|
+
deadlineMs = REFRESH_DEADLINE_MS;
|
|
76
|
+
refresh;
|
|
77
|
+
constructor(refresh = runFleetCacheWarmTick) {
|
|
78
|
+
super();
|
|
79
|
+
this.refresh = refresh;
|
|
21
80
|
}
|
|
22
|
-
async
|
|
23
|
-
|
|
24
|
-
|
|
81
|
+
async onStart() { }
|
|
82
|
+
async onStop() { }
|
|
83
|
+
async onTick(_ctx, signal) {
|
|
84
|
+
await Promise.race([
|
|
85
|
+
this.refresh(signal),
|
|
86
|
+
abortRejection(signal, 'auth refresh aborted at deadline'),
|
|
87
|
+
]);
|
|
25
88
|
}
|
|
26
89
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reserved `auth` bundle fleet sync as a `PeriodicService` (PHNX-2371).
|
|
3
3
|
*
|
|
4
|
-
* Each daemon
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each daemon publishes a safe readiness verdict to the fleet-shared user repo,
|
|
5
|
+
* then runs the same serialized, timeout-bounded git exchange as usage sync.
|
|
6
|
+
* One deterministic ready device asynchronously provisions peers whose delivered
|
|
7
|
+
* verdict says `missing`; the secret never enters Git.
|
|
8
8
|
*/
|
|
9
9
|
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
10
10
|
import type { DaemonServiceId } from '../daemon-services.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reserved `auth` bundle fleet sync as a `PeriodicService` (PHNX-2371).
|
|
3
3
|
*
|
|
4
|
-
* Each daemon
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each daemon publishes a safe readiness verdict to the fleet-shared user repo,
|
|
5
|
+
* then runs the same serialized, timeout-bounded git exchange as usage sync.
|
|
6
|
+
* One deterministic ready device asynchronously provisions peers whose delivered
|
|
7
|
+
* verdict says `missing`; the secret never enters Git.
|
|
8
8
|
*/
|
|
9
9
|
import { BasePeriodicService } from './service.js';
|
|
10
10
|
const AUTH_SYNC_TICK_MS = 15 * 60_000;
|
|
@@ -22,8 +22,19 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
22
22
|
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
23
23
|
}
|
|
24
24
|
async onTick(ctx) {
|
|
25
|
-
const { syncReservedAuthBundle } = await import('../secrets/reserved-sync.js');
|
|
26
|
-
const
|
|
25
|
+
const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets/reserved-sync.js');
|
|
26
|
+
const published = publishReservedAuthVerdict();
|
|
27
|
+
if (published.error)
|
|
28
|
+
ctx.log('WARN', `auth-sync: verdict: ${published.error}`);
|
|
29
|
+
const { syncFleetSharedStateRepo } = await import('../fleet-shared-repo-sync.js');
|
|
30
|
+
const transport = await syncFleetSharedStateRepo();
|
|
31
|
+
if (transport.skipped)
|
|
32
|
+
ctx.log('WARN', `auth-sync: ${transport.skipped}`);
|
|
33
|
+
if (transport.error)
|
|
34
|
+
ctx.log('WARN', `auth-sync: shared-store transport: ${transport.error}`);
|
|
35
|
+
if (!transport.success)
|
|
36
|
+
return;
|
|
37
|
+
const result = await syncReservedAuthBundle();
|
|
27
38
|
if (result.pushed.length > 0) {
|
|
28
39
|
ctx.log('INFO', `auth-sync: pushed auth to ${result.pushed.join(', ')}`);
|
|
29
40
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catch-up recovery as a supervised `PeriodicService` (PHNX-3608).
|
|
3
|
+
*
|
|
4
|
+
* A catch-up pass detects routines whose scheduled fire this device missed (the
|
|
5
|
+
* laptop slept, the daemon was wedged, an OS suspend the process survived) and
|
|
6
|
+
* runs them late. It used to run on a bare `setInterval` booted/stopped inside
|
|
7
|
+
* `runDaemon()` alongside the scheduler, with only a local `catchingUp` overlap
|
|
8
|
+
* flag and NO deadline — a pass that hung on an off-box (host/cloud) dispatch
|
|
9
|
+
* could latch that flag and silently stop recovering missed fires for the
|
|
10
|
+
* daemon's life, the same "abstraction too weak" class the supervisor exists for.
|
|
11
|
+
*
|
|
12
|
+
* Under the supervisor it gets a per-tick deadline, an AbortSignal, and the
|
|
13
|
+
* circuit breaker: a hung pass is abandoned and restarted on backoff instead of
|
|
14
|
+
* latching. The scheduler itself (croner-driven `JobScheduler`) stays outside the
|
|
15
|
+
* supervisor — it is not a tick loop — so this service self-gates on whether the
|
|
16
|
+
* scheduler is currently booted and no-ops cheaply when the scheduler gate is off.
|
|
17
|
+
*/
|
|
18
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
19
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
20
|
+
/**
|
|
21
|
+
* How often to re-run catch-up. A startup pass alone misses a fire lost while the
|
|
22
|
+
* daemon stayed up but its event loop was wedged, or one lost across an OS
|
|
23
|
+
* suspend the process survived — five minutes bounds the cost while still
|
|
24
|
+
* recovering from a wedge or an OS suspend the process survived.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CATCHUP_TICK_MS: number;
|
|
27
|
+
export interface CatchupServiceDeps {
|
|
28
|
+
/**
|
|
29
|
+
* Whether the routine scheduler is currently booted. When false (the
|
|
30
|
+
* `scheduler.enabled` gate is off), the pass no-ops — there is nothing to
|
|
31
|
+
* catch up on a device where no routines fire.
|
|
32
|
+
*/
|
|
33
|
+
isSchedulerBooted: () => boolean;
|
|
34
|
+
/** Run one catch-up pass. Receives the tick's AbortSignal so it can bound its dispatches. */
|
|
35
|
+
runPass: (signal: AbortSignal) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare class CatchupService extends BasePeriodicService {
|
|
38
|
+
private readonly deps;
|
|
39
|
+
readonly id: DaemonServiceId;
|
|
40
|
+
readonly intervalMs: number;
|
|
41
|
+
/**
|
|
42
|
+
* A pass awaits `executeJobDetached` per overdue job and an off-box dispatch
|
|
43
|
+
* can block for a while, so the bound is generous — but finite, which the old
|
|
44
|
+
* un-deadlined loop was not. A pass exceeding it is hung, not slow.
|
|
45
|
+
*/
|
|
46
|
+
readonly deadlineMs: number;
|
|
47
|
+
constructor(deps: CatchupServiceDeps);
|
|
48
|
+
protected onStart(): Promise<void>;
|
|
49
|
+
protected onStop(): Promise<void>;
|
|
50
|
+
protected onTick(_ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catch-up recovery as a supervised `PeriodicService` (PHNX-3608).
|
|
3
|
+
*
|
|
4
|
+
* A catch-up pass detects routines whose scheduled fire this device missed (the
|
|
5
|
+
* laptop slept, the daemon was wedged, an OS suspend the process survived) and
|
|
6
|
+
* runs them late. It used to run on a bare `setInterval` booted/stopped inside
|
|
7
|
+
* `runDaemon()` alongside the scheduler, with only a local `catchingUp` overlap
|
|
8
|
+
* flag and NO deadline — a pass that hung on an off-box (host/cloud) dispatch
|
|
9
|
+
* could latch that flag and silently stop recovering missed fires for the
|
|
10
|
+
* daemon's life, the same "abstraction too weak" class the supervisor exists for.
|
|
11
|
+
*
|
|
12
|
+
* Under the supervisor it gets a per-tick deadline, an AbortSignal, and the
|
|
13
|
+
* circuit breaker: a hung pass is abandoned and restarted on backoff instead of
|
|
14
|
+
* latching. The scheduler itself (croner-driven `JobScheduler`) stays outside the
|
|
15
|
+
* supervisor — it is not a tick loop — so this service self-gates on whether the
|
|
16
|
+
* scheduler is currently booted and no-ops cheaply when the scheduler gate is off.
|
|
17
|
+
*/
|
|
18
|
+
import { BasePeriodicService } from './service.js';
|
|
19
|
+
/**
|
|
20
|
+
* How often to re-run catch-up. A startup pass alone misses a fire lost while the
|
|
21
|
+
* daemon stayed up but its event loop was wedged, or one lost across an OS
|
|
22
|
+
* suspend the process survived — five minutes bounds the cost while still
|
|
23
|
+
* recovering from a wedge or an OS suspend the process survived.
|
|
24
|
+
*/
|
|
25
|
+
export const CATCHUP_TICK_MS = 5 * 60_000;
|
|
26
|
+
export class CatchupService extends BasePeriodicService {
|
|
27
|
+
deps;
|
|
28
|
+
id = 'catchup';
|
|
29
|
+
intervalMs = CATCHUP_TICK_MS;
|
|
30
|
+
/**
|
|
31
|
+
* A pass awaits `executeJobDetached` per overdue job and an off-box dispatch
|
|
32
|
+
* can block for a while, so the bound is generous — but finite, which the old
|
|
33
|
+
* un-deadlined loop was not. A pass exceeding it is hung, not slow.
|
|
34
|
+
*/
|
|
35
|
+
deadlineMs = 4 * 60_000;
|
|
36
|
+
constructor(deps) {
|
|
37
|
+
super();
|
|
38
|
+
this.deps = deps;
|
|
39
|
+
}
|
|
40
|
+
async onStart() {
|
|
41
|
+
// No owned resources — the supervisor owns the timer.
|
|
42
|
+
}
|
|
43
|
+
async onStop() {
|
|
44
|
+
// No owned resources.
|
|
45
|
+
}
|
|
46
|
+
async onTick(_ctx, signal) {
|
|
47
|
+
if (!this.deps.isSchedulerBooted())
|
|
48
|
+
return;
|
|
49
|
+
await this.deps.runPass(signal);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -120,8 +120,11 @@ export declare function removeHeartbeat(): void;
|
|
|
120
120
|
export declare function isDaemonWedged(): boolean;
|
|
121
121
|
/**
|
|
122
122
|
* Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
|
|
123
|
-
* the pid file has been lost (see resolveLiveDaemonPid).
|
|
124
|
-
*
|
|
123
|
+
* the pid file has been lost (see resolveLiveDaemonPid). The observation itself
|
|
124
|
+
* is read-only; when it finds desync it opportunistically acquires daemon.lock
|
|
125
|
+
* and repeats the observation there before repairing. A contended probe still
|
|
126
|
+
* returns the observed liveness without mutating another lifecycle operation's
|
|
127
|
+
* state.
|
|
125
128
|
*/
|
|
126
129
|
export declare function isDaemonRunning(): boolean;
|
|
127
130
|
/**
|
|
@@ -137,9 +140,9 @@ export declare function isDaemonRunning(): boolean;
|
|
|
137
140
|
* pid file, this does NOT defer to it — it evicts the incumbent and becomes the
|
|
138
141
|
* survivor, so a second install can never leave two daemons running. Returns true
|
|
139
142
|
* and records our PID once the incumbent is provably dead (its resources
|
|
140
|
-
* released). Returns false
|
|
141
|
-
* O_EXCL start lock
|
|
142
|
-
*
|
|
143
|
+
* released). Returns false when another `__daemon-run` currently holds the
|
|
144
|
+
* O_EXCL start lock, or when the incumbent cannot be safely identified/evicted,
|
|
145
|
+
* in which case the caller must exit without touching further state.
|
|
143
146
|
* The read-evict-write is serialized behind the same start lock startDaemon()
|
|
144
147
|
* uses, so two `_run` processes can't both claim in the window between the
|
|
145
148
|
* liveness check and the write.
|
|
@@ -177,8 +180,11 @@ export declare function reapStrayDaemons(keepPid?: number): {
|
|
|
177
180
|
reaped: number;
|
|
178
181
|
details: string[];
|
|
179
182
|
};
|
|
183
|
+
export declare function isLiveDaemon(pid: number): boolean;
|
|
180
184
|
/** Append a JSONL log entry to the daemon log file (owner-only permissions). */
|
|
181
185
|
export declare function log(level: string, message: string): void;
|
|
186
|
+
/** Keep synchronous signal-handler failures inside the daemon's crash barrier. */
|
|
187
|
+
export declare function guardSignalHandler(handler: () => void, onError: (err: unknown) => void): () => void;
|
|
182
188
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
183
189
|
/**
|
|
184
190
|
* Anchor the daemon's working directory to a stable, always-present path.
|
|
@@ -390,7 +396,7 @@ export declare function startDetached(opts?: StartDetachedOptions): {
|
|
|
390
396
|
export interface StopResidueArtifact {
|
|
391
397
|
label: string;
|
|
392
398
|
present: boolean;
|
|
393
|
-
/** The file names a
|
|
399
|
+
/** The file names a live owner, including a stopped target that survived. */
|
|
394
400
|
ownedByLiveOther: boolean;
|
|
395
401
|
reclaim: () => void;
|
|
396
402
|
stillPresent: () => boolean;
|
|
@@ -424,22 +430,6 @@ export interface DaemonStopResult {
|
|
|
424
430
|
surviving: string[];
|
|
425
431
|
detachedChildren: number[];
|
|
426
432
|
}
|
|
427
|
-
/**
|
|
428
|
-
* Live `__daemon-run` processes still registered in THIS state dir's instance
|
|
429
|
-
* registry, excluding `exclude`. State-dir-scoped by construction: the registry
|
|
430
|
-
* lives inside this state dir, so a daemon serving a DIFFERENT state dir (a test
|
|
431
|
-
* fixture with its own HOME, a separate install/home) registers elsewhere and is
|
|
432
|
-
* invisible here — it is never a stop/takeover target. POSIX-only (the registry
|
|
433
|
-
* and its `ps` liveness probe are); `[]` on Windows.
|
|
434
|
-
*
|
|
435
|
-
* Exported for `agents daemon status`/`doctor`/`services` (RUSH-2368): those
|
|
436
|
-
* commands previously flagged every `__daemon-run` on the box (a raw `ps` scan)
|
|
437
|
-
* as a "duplicate" of this daemon, which misreported test fixtures under their
|
|
438
|
-
* own HOME — and therefore their own state dir and registry — as strays to
|
|
439
|
-
* kill. This registry read is the same scope the reaper (`reapStrayDaemons`)
|
|
440
|
-
* and the stop postcondition (`stopDaemon`) already use, so the display and the
|
|
441
|
-
* reaper agree on what a duplicate is.
|
|
442
|
-
*/
|
|
443
433
|
export declare function findSurvivingStateDirDaemons(exclude: Set<number>): number[];
|
|
444
434
|
/**
|
|
445
435
|
* Stop the daemon and ASSERT its postcondition (SING-12, RUSH-2355), unloading it
|