@phnx-labs/agents-cli 1.22.53 → 1.22.55

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.
Files changed (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -17,22 +17,19 @@ import { listJobs as listAllJobs } from '../scheduling/routines.js';
17
17
  import { syncAllProjectRoutines } from '../routines-project.js';
18
18
  import { JobScheduler } from '../scheduler.js';
19
19
  // MonitorEngine is now owned by MonitorEngineService (daemon/monitor-engine-service.ts).
20
- import { executeJobDetached, monitorRunningJobs, listLiveRoutineChildren } from './runner.js';
20
+ import { executeJobDetached, listLiveRoutineChildren } from './runner.js';
21
21
  import { detectOverdueJobs, notifyOverdue } from '../overdue.js';
22
22
  import { runCatchup } from '../catchup.js';
23
23
  import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } from '../routine-notify.js';
24
24
  import { notifyOwnerRoutineFinish, notifyOwnerRoutineStartFailed } from '../routine-notify-owner.js';
25
25
  import { BrowserService } from '../browser/service.js';
26
26
  import { getSocketPath as getBrowserIpcSocketPath } from '../browser/ipc.js';
27
- import { startHostedWebhookReceivers } from '../daemon-webhooks.js';
28
27
  import { secretsBrokerSocketPath, brokerPidAlive } from '../secrets/agent.js';
29
28
  import { redactSecrets } from '../redact.js';
30
29
  import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from '../cli-entry.js';
31
- import { isSchedulerEnabled, assertSchedulerEnabled, isDaemonEnabled, resolveBrowserTaskIdleMs } from '../device-config.js';
32
- import { reapTerminalRoutineProcesses } from '../routine-process-cleanup.js';
30
+ import { localBinDir } from '../platform/posixpath.js';
31
+ import { isSchedulerEnabled, assertSchedulerEnabled, isDaemonEnabled } from '../device-config.js';
33
32
  import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, readSubsystemHealth, SUBSYSTEM_DAEMON_START } from '../daemon-health.js';
34
- import { runActiveSessionsWarmTick } from '../daemon-ticks.js';
35
- import { watchActiveSessionsReaderPresence } from '../session/session-cache.js';
36
33
  import { ServiceSupervisor } from './supervisor.js';
37
34
  import { SessionIndexService } from './session-index-service.js';
38
35
  import { SecretsBrokerService } from './secrets-broker-service.js';
@@ -43,7 +40,13 @@ import { WatchdogService } from './watchdog-service.js';
43
40
  import { DeviceProbeService } from './device-probe-service.js';
44
41
  import { SelfHealService } from './self-heal-service.js';
45
42
  import { KeychainReapService } from './keychain-reap-service.js';
43
+ import { AuthSyncService } from './auth-sync-service.js';
46
44
  import { StateDirCheckService } from './state-dir-check-service.js';
45
+ import { SessionStateService } from './session-state-service.js';
46
+ import { WebhookReceiverService } from './webhook-receiver-service.js';
47
+ import { HeartbeatService } from './heartbeat-service.js';
48
+ import { TmuxReapService } from './tmux-reap-service.js';
49
+ import { BrowserTaskReapService } from './browser-task-reap-service.js';
47
50
  import { emit, emitRoutineEnd } from '../feed/events.js';
48
51
  import { readDaemonServicesConfig, drainDaemonServiceRestartQueue } from '../daemon-services.js';
49
52
  /**
@@ -110,7 +113,6 @@ export function daemonSystemdUnitName() {
110
113
  const suffix = isolatedHomeSuffix();
111
114
  return suffix ? `agents-daemon-sandbox-${suffix}.service` : SYSTEMD_UNIT;
112
115
  }
113
- const MONITOR_TICK_MS = 60_000;
114
116
  /**
115
117
  * How often to re-scan for missed fires. Deliberately slower than the monitor
116
118
  * tick: detection walks a week of cron occurrences per routine
@@ -134,17 +136,10 @@ const CATCHUP_TICK_MS = 5 * 60_000;
134
136
  * from wedges every keychain-backed secret on the box, so it runs minutely.
135
137
  */
136
138
  // BROKER_SELF_HEAL_TICK_MS moved to secrets-broker-service.ts (RUSH-3193 P2).
137
- // RUSH-2501: reap tmux sessions whose panes are all dead every 5 minutes.
138
- const DEAD_PANE_REAP_TICK_MS = 5 * 60_000;
139
- // RUSH-2622: close abandoned browser-task tabs every 5 minutes.
140
- const BROWSER_TASK_REAP_TICK_MS = 5 * 60_000;
141
- // Keep the active-session cache/journal fresh for sessions watch + Factory.
142
- // Matches DEFAULT_ACTIVE_CACHE_MAX_AGE_MS (15s) so long-lived watchers never
143
- // outlive the publisher (RUSH-2484 — CLI-owned continuous publish).
144
- const ACTIVE_SESSIONS_WARM_TICK_MS = 15_000;
145
139
  // Session-index warm interval/deadline live in session-index-service.ts now
146
140
  // (RUSH-3193 — migrated onto ServiceSupervisor).
147
141
  const WEDGE_THRESHOLD_TICKS = 3;
142
+ const DAEMON_HEARTBEAT_TICK_MS = 60_000;
148
143
  /**
149
144
  * Crash-loop prevention (RUSH-2418). Three layers, because none of them alone
150
145
  * bounds a daemon that dies during startup:
@@ -346,7 +341,7 @@ export function removeHeartbeat() {
346
341
  */
347
342
  function isHeartbeatFresh(hb) {
348
343
  const elapsed = Date.now() - Date.parse(hb.lastTick);
349
- return elapsed <= WEDGE_THRESHOLD_TICKS * MONITOR_TICK_MS;
344
+ return elapsed <= WEDGE_THRESHOLD_TICKS * DAEMON_HEARTBEAT_TICK_MS;
350
345
  }
351
346
  export function isDaemonWedged() {
352
347
  const pid = readDaemonPid();
@@ -723,87 +718,6 @@ export function warnEphemeralDaemonRoot(resolveBin = getAgentsBinPath) {
723
718
  // registered after `handleShutdown` is declared — see its registration site
724
719
  // below. runBrokerSelfHeal was moved into SecretsBrokerService (RUSH-3193 P2).
725
720
  // ---------------------------------------------------------------------------
726
- // In-flight guards: each flag prevents a slow tick from being re-entered
727
- // by the next timer fire before the previous one finishes.
728
- let reapingDeadPanes = false;
729
- let reapingBrowserTasks = false;
730
- /**
731
- * RUSH-2501: kill tmux sessions on the helper socket whose panes are ALL dead.
732
- * RUSH-2521: and terminate the helper processes (MCP servers, harness background
733
- * daemons) whose owning agent has exited — killing the pane only SIGHUPs its
734
- * foreground process group, so anything that left that group outlives it.
735
- *
736
- * Runs every DEAD_PANE_REAP_TICK_MS (5 min). The daemon is the single executor
737
- * so no UI surface can race it.
738
- */
739
- async function runDeadPaneReap() {
740
- if (reapingDeadPanes)
741
- return;
742
- reapingDeadPanes = true;
743
- try {
744
- const { reapDeadTmuxPanes } = await import('../tmux/session.js');
745
- const { getDefaultSocketPath } = await import('../tmux/paths.js');
746
- const result = await reapDeadTmuxPanes(getDefaultSocketPath());
747
- for (const w of result.warnings)
748
- log('WARN', `Dead-pane reaper: ${w}`);
749
- if (result.processes > 0) {
750
- log('INFO', `Dead-pane reaper: terminated ${result.processes} orphaned helper process(es)`);
751
- for (const d of result.processDetails)
752
- log('INFO', ` ${d}`);
753
- }
754
- if (result.reaped > 0) {
755
- log('INFO', `Dead-pane reaper: reaped ${result.reaped} session(s)`);
756
- for (const d of result.details)
757
- log('INFO', ` killed ${d}`);
758
- }
759
- }
760
- catch (err) {
761
- log('ERROR', `Dead-pane reaper failed: ${err.message}`);
762
- }
763
- finally {
764
- reapingDeadPanes = false;
765
- }
766
- }
767
- /**
768
- * RUSH-2622: close abandoned browser-task tabs — a task whose owning agent
769
- * session has exited, or one idle past `browser.task-idle-minutes` (default 30,
770
- * 0 disables idle reaping only). Same policy `agents browser prune` triggers on
771
- * demand; this is the periodic side, and the daemon is the single executor so
772
- * no UI surface can race it. `service` is the daemon's own long-lived
773
- * BrowserService — the browser IPC server it started earlier in `runDaemon()`.
774
- *
775
- * Runs every BROWSER_TASK_REAP_TICK_MS (5 min).
776
- */
777
- async function runBrowserTaskReap(service) {
778
- if (reapingBrowserTasks)
779
- return;
780
- reapingBrowserTasks = true;
781
- try {
782
- const result = await service.reapAbandoned({ idleMs: resolveBrowserTaskIdleMs() });
783
- if (result.closed.length > 0) {
784
- log('INFO', `Browser-task reaper: closed ${result.closed.length} task(s)`);
785
- for (const c of result.closed)
786
- log('INFO', ` ${c.task} (${c.reason}, profile ${c.profile})`);
787
- }
788
- }
789
- catch (err) {
790
- log('ERROR', `Browser-task reaper failed: ${err.message}`);
791
- }
792
- finally {
793
- reapingBrowserTasks = false;
794
- }
795
- }
796
- /**
797
- * One monitor tick: write heartbeat + drain the running-job monitor + reap
798
- * stale terminal routine process groups. Runs on MONITOR_TICK_MS.
799
- */
800
- function runMonitorTick() {
801
- writeHeartbeat();
802
- monitorRunningJobs();
803
- const reaped = reapTerminalRoutineProcesses();
804
- if (reaped.length > 0)
805
- log('WARN', `Reaped ${reaped.length} terminal routine process group(s): ${reaped.join(', ')}`);
806
- }
807
721
  export async function runDaemon() {
808
722
  // Single-instance guard (last-wins, SING-11): a direct `agents __daemon-run`
809
723
  // (manual, or a service-manager restart racing a live predecessor) EVICTS the
@@ -889,6 +803,11 @@ export async function runDaemon() {
889
803
  // broker starts socket-first (#416) and `agents secrets` resolves within
890
804
  // ms of daemon start, exactly as before.
891
805
  const supervisor = new ServiceSupervisor();
806
+ if (isEnabled('session-state')) {
807
+ supervisor.register(new SessionStateService(() => supervisor.runNow('session-state')));
808
+ }
809
+ else
810
+ log('INFO', 'Live session-state service disabled');
892
811
  if (isEnabled('secrets-broker'))
893
812
  supervisor.register(new SecretsBrokerService());
894
813
  else
@@ -902,12 +821,13 @@ export async function runDaemon() {
902
821
  supervisor.register(new AccountStateDaemonService());
903
822
  else
904
823
  log('INFO', 'Account-state service disabled');
905
- // BrowserIPCService owns the orphan reap (previously daemon.ts:1303-1318)
906
- // and the BrowserIPCServer lifecycle. BrowserService is also constructed here
907
- // so daemon.ts can access it for the browser-task-reap interval (task #3).
908
- const browserSvcForReap = isEnabled('browser-ipc') ? new BrowserService() : null;
909
- if (browserSvcForReap)
910
- supervisor.register(new BrowserIPCService(browserSvcForReap));
824
+ // BrowserIPCService and BrowserTaskReapService share one long-lived
825
+ // BrowserService when both are enabled. Either service can run independently.
826
+ const browserService = isEnabled('browser-ipc') || isEnabled('browser-task-reap')
827
+ ? new BrowserService()
828
+ : null;
829
+ if (isEnabled('browser-ipc') && browserService)
830
+ supervisor.register(new BrowserIPCService(browserService));
911
831
  else
912
832
  log('INFO', 'Browser IPC service disabled');
913
833
  if (isEnabled('session-index'))
@@ -934,6 +854,28 @@ export async function runDaemon() {
934
854
  supervisor.register(new KeychainReapService());
935
855
  else
936
856
  log('INFO', 'Keychain-reap service disabled');
857
+ if (isEnabled('auth-sync'))
858
+ supervisor.register(new AuthSyncService());
859
+ else
860
+ log('INFO', 'Auth-sync service disabled');
861
+ if (isEnabled('webhook-receiver'))
862
+ supervisor.register(new WebhookReceiverService());
863
+ else
864
+ log('INFO', 'Webhook receiver service disabled');
865
+ if (isEnabled('daemon-heartbeat'))
866
+ supervisor.register(new HeartbeatService(() => writeHeartbeat()));
867
+ else
868
+ log('INFO', 'Daemon heartbeat service disabled');
869
+ if (isEnabled('tmux-reap'))
870
+ supervisor.register(new TmuxReapService());
871
+ else
872
+ log('INFO', 'Tmux reap service disabled');
873
+ if (isEnabled('browser-task-reap') && browserService) {
874
+ supervisor.register(new BrowserTaskReapService(browserService));
875
+ }
876
+ else {
877
+ log('INFO', 'Browser-task reap service disabled');
878
+ }
937
879
  await supervisor.startAll({ log });
938
880
  activeServiceSupervisor = supervisor;
939
881
  // scheduler.enabled=false in this machine's device doc means NO routines fire
@@ -1070,36 +1012,9 @@ export async function runDaemon() {
1070
1012
  }
1071
1013
  if (schedulerEnabledAtBoot)
1072
1014
  bootScheduler();
1073
- // Active-sessions publish-own: continuous journal writer for `sessions watch`.
1074
- // Fire once immediately so a cold daemon does not leave watchers on
1075
- // "awaiting publisher" for a full tick (RUSH-2484). This boot-time fire alone
1076
- // no-ops on a genuinely cold boot (no reader has connected yet) — the
1077
- // presence watch below is what actually closes the gap for a watcher that
1078
- // connects later, whether at cold boot or after the reader-idle window.
1079
- let activeSessionsWarmInFlight = false;
1080
- const runActiveSessionsWarm = async () => {
1081
- if (activeSessionsWarmInFlight)
1082
- return;
1083
- activeSessionsWarmInFlight = true;
1084
- try {
1085
- await runActiveSessionsWarmTick();
1086
- }
1087
- catch (err) {
1088
- log('WARN', `active-sessions warm failed: ${err.message}`);
1089
- }
1090
- finally {
1091
- activeSessionsWarmInFlight = false;
1092
- }
1093
- };
1094
- void runActiveSessionsWarm();
1095
- const activeSessionsWarmInterval = setInterval(() => { void runActiveSessionsWarm(); }, ACTIVE_SESSIONS_WARM_TICK_MS);
1096
- // Out-of-band trigger (RUSH-2484): a reader connecting only writes a presence
1097
- // timestamp (noteActiveSessionsJournalReader in watch.ts) with no path back to
1098
- // this timer, so on its own the interval above leaves a fresh connection
1099
- // waiting up to ACTIVE_SESSIONS_WARM_TICK_MS for its first rows. This watches
1100
- // for the idle->recent edge and gathers immediately instead of waiting for the
1101
- // next scheduled tick — a genuinely idle box with no reader still never gathers.
1102
- const stopActiveSessionsReaderWatch = watchActiveSessionsReaderPresence(() => { void runActiveSessionsWarm(); });
1015
+ // Live-session metadata publishing is owned by SessionStateService. Its
1016
+ // presence watcher requests an immediate supervised tick when a reader
1017
+ // connects; callers consume the journal instead of duplicating the gather.
1103
1018
  // Session-index warm (RUSH-2682) is registered on the supervisor above
1104
1019
  // (RUSH-3193 P2) alongside the socket services.
1105
1020
  // Watchdog and device-probe are now managed by WatchdogService /
@@ -1185,23 +1100,8 @@ export async function runDaemon() {
1185
1100
  // Started after the broker (above) so it resolves each receiver's signing
1186
1101
  // secret headlessly — no AGENTS_SECRETS_PASSPHRASE, no nohup. Binds nothing
1187
1102
  // unless daemon/webhooks.yaml declares a receiver, so an unconfigured box no-ops.
1188
- let webhookReceivers = null;
1189
- if (isEnabled('webhook-receiver')) {
1190
- try {
1191
- webhookReceivers = await startHostedWebhookReceivers({ log });
1192
- log('INFO', webhookReceivers.count > 0
1193
- ? `Webhook receiver hosting ${webhookReceivers.count} receiver(s)`
1194
- : 'Webhook receiver service enabled; no receivers declared in daemon/webhooks.yaml');
1195
- }
1196
- catch (err) {
1197
- log('WARN', `Webhook receiver host skipped: ${err.message}`);
1198
- }
1199
- }
1200
- else {
1201
- log('INFO', 'Webhook receiver service disabled');
1202
- }
1203
- runMonitorTick();
1204
- const monitorInterval = setInterval(runMonitorTick, MONITOR_TICK_MS);
1103
+ // Signed webhook ingress is owned by WebhookReceiverService, including
1104
+ // per-service failure isolation, measured health, and shutdown cleanup.
1205
1105
  // Resource self-heal is now managed by SelfHealService on the supervisor
1206
1106
  // (RUSH-3193 P3), registered above alongside the socket services. The
1207
1107
  // supervisor's immediate first tick on start replaces the old
@@ -1212,16 +1112,11 @@ export async function runDaemon() {
1212
1112
  // supervisor (RUSH-3193 P3), registered above alongside the socket services.
1213
1113
  // RUSH-2501: reap tmux sessions whose panes are all dead. Runs on the same
1214
1114
  // 5-min cadence as the keychain reaper. Daemon-only (single executor).
1215
- void runDeadPaneReap(); // kick-off on startup so the backlog clears immediately
1216
- const deadPaneReapInterval = setInterval(() => { void runDeadPaneReap(); }, DEAD_PANE_REAP_TICK_MS);
1115
+ // Dead managed panes and their orphan helpers are reaped by TmuxReapService.
1217
1116
  // RUSH-2622: close abandoned browser-task tabs on the same 5-min cadence,
1218
- // reusing the daemon's own long-lived BrowserService. `browserSvcForReap` is
1219
- // the BrowserService created for BrowserIPCService above; null when disabled.
1220
- let browserTaskReapInterval;
1221
- if (browserSvcForReap) {
1222
- void runBrowserTaskReap(browserSvcForReap); // kick-off on startup
1223
- browserTaskReapInterval = setInterval(() => { void runBrowserTaskReap(browserSvcForReap); }, BROWSER_TASK_REAP_TICK_MS);
1224
- }
1117
+ // reusing the daemon's long-lived BrowserService when browser IPC is enabled.
1118
+ // Abandoned browser tasks are reaped by BrowserTaskReapService, sharing the
1119
+ // same BrowserService instance as BrowserIPCService.
1225
1120
  // RUSH-2367 / RUSH-3193 P3: state-dir-check (self-terminate guard) is
1226
1121
  // registered on the supervisor further below, once `handleShutdown` exists
1227
1122
  // — see the registration site after its declaration for why.
@@ -1259,7 +1154,11 @@ export async function runDaemon() {
1259
1154
  // never registered, so it falls through to the same "restart to
1260
1155
  // apply" advice as before.
1261
1156
  if (supervisor.isRegistered(id)) {
1262
- const action = now ? supervisor.start(id) : supervisor.stop(id);
1157
+ // A periodic service may be inside a real tick when SIGHUP arrives.
1158
+ // Queue the desired transition behind that exact promise: deadlines
1159
+ // detect a wedge but cannot cancel arbitrary work, and polling here
1160
+ // would create another lifecycle timer outside the supervisor.
1161
+ const action = supervisor.awaitIdle(id).then(() => now ? supervisor.start(id) : supervisor.stop(id));
1263
1162
  void action
1264
1163
  .then(() => log('INFO', `Service '${id}' ${now ? 'started' : 'stopped'} live (SIGHUP reload)`))
1265
1164
  .catch((err) => log('WARN', `Service '${id}' live ${now ? 'start' : 'stop'} failed: ${err.message}`));
@@ -1274,7 +1173,7 @@ export async function runDaemon() {
1274
1173
  // Drain queued `agents daemon services restart <id>` requests (RUSH-3193 P4).
1275
1174
  for (const id of drainDaemonServiceRestartQueue()) {
1276
1175
  if (supervisor.isRegistered(id)) {
1277
- void supervisor.restartOne(id)
1176
+ void supervisor.awaitIdle(id).then(() => supervisor.restartOne(id))
1278
1177
  .then(() => log('INFO', `Service '${id}' restarted live (SIGHUP reload)`))
1279
1178
  .catch((err) => log('WARN', `Service '${id}' live restart failed: ${err.message}`));
1280
1179
  }
@@ -1345,20 +1244,12 @@ export async function runDaemon() {
1345
1244
  // rather than one that every step added later has to re-earn.
1346
1245
  const handleShutdown = singleShot(async () => {
1347
1246
  log('INFO', 'Daemon shutting down');
1348
- clearInterval(activeSessionsWarmInterval);
1349
- stopActiveSessionsReaderWatch();
1350
- // supervisor.stopAll() stops every registered service: secrets-broker
1351
- // (closes hostedBroker + self-heal timer), monitor engine, account-state,
1352
- // browser IPC, session-index, watchdog, device-probe, self-heal,
1353
- // keychain-reap, and (once registered just below) state-dir-check.
1247
+ // supervisor.stopAll() stops every registered service, including socket
1248
+ // hosts, session publishers/indexers, monitor/account work, and every
1249
+ // maintenance timer; state-dir-check joins the registry just below.
1354
1250
  await supervisor.stopAll();
1355
1251
  activeServiceSupervisor = null;
1356
1252
  stopScheduler();
1357
- await webhookReceivers?.close();
1358
- clearInterval(monitorInterval);
1359
- clearInterval(deadPaneReapInterval);
1360
- if (browserTaskReapInterval)
1361
- clearInterval(browserTaskReapInterval);
1362
1253
  try {
1363
1254
  if (fs.readFileSync(lifetimePath, 'utf-8') === lifetimeToken)
1364
1255
  fs.unlinkSync(lifetimePath);
@@ -1800,10 +1691,23 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
1800
1691
  function daemonNodeBinDir() {
1801
1692
  return path.dirname(process.execPath);
1802
1693
  }
1694
+ /**
1695
+ * Login-shell user-bin dirs a service-manager-started daemon would otherwise
1696
+ * miss. systemd/launchd pin PATH and never source `~/.profile`, so they never
1697
+ * see `~/.rush/bin` (where `rush` lands) or `~/.local/bin` (XDG user-bin).
1698
+ * Monitor `notify` and dispatched `agents run` children inherit this PATH;
1699
+ * without these dirs the rush-backed owner channel fails with
1700
+ * `rush CLI not found on PATH` (PHNX-3075) while an interactive shell on the
1701
+ * same box succeeds. Uses the same HOME the manifest bakes (RUSH-2639).
1702
+ */
1703
+ function daemonUserBinDirs() {
1704
+ const home = serviceManifestHomeEnv().HOME;
1705
+ return [path.join(home, '.rush', 'bin'), localBinDir(home)];
1706
+ }
1803
1707
  /**
1804
1708
  * The full PATH value the daemon service manifest pins, in order: the directory
1805
- * of the `agents` shim itself FIRST, then the Node runtime dir, then the
1806
- * platform's system dirs.
1709
+ * of the `agents` shim itself FIRST, then the Node runtime dir, then login-shell
1710
+ * user-bin dirs (`~/.rush/bin`, `~/.local/bin`), then the platform's system dirs.
1807
1711
  *
1808
1712
  * The shim's own dir must lead so a scheduled `command` routine that shells out
1809
1713
  * to the bare name `agents` (`/bin/sh -c 'agents repo pull system'`) resolves the
@@ -1814,12 +1718,19 @@ function daemonNodeBinDir() {
1814
1718
  *
1815
1719
  * The Node runtime dir stays second so the shim's shebang (`#!/usr/bin/env node`)
1816
1720
  * still resolves the exact Node that installed the service — never an ancient
1817
- * system node or a pruned nvm version. Deduped across the whole list, so a
1721
+ * system node or a pruned nvm version. User-bin dirs sit after that pair so a
1722
+ * `~/.local/bin/agents` cannot shadow the daemon binary, but `rush` (and other
1723
+ * login-shell CLIs) still resolve. Deduped across the whole list, so a
1818
1724
  * Node/shim dir that already appears among the system dirs (e.g. a
1819
1725
  * `/usr/local/bin` install) never doubles.
1820
1726
  */
1821
1727
  function daemonPathValue(agentsBin, systemDirs) {
1822
- return [...new Set([path.dirname(agentsBin), daemonNodeBinDir(), ...systemDirs])].join(':');
1728
+ return [...new Set([
1729
+ path.dirname(agentsBin),
1730
+ daemonNodeBinDir(),
1731
+ ...daemonUserBinDirs(),
1732
+ ...systemDirs,
1733
+ ])].join(':');
1823
1734
  }
1824
1735
  /**
1825
1736
  * Build the argv to relaunch the `agents` CLI with the given subcommand args.
@@ -0,0 +1,13 @@
1
+ /** Daemon heartbeat and routine-process reconciliation under supervision. */
2
+ import type { DaemonServiceId } from '../daemon-services.js';
3
+ import { BasePeriodicService, type DaemonContext } from './service.js';
4
+ export declare class HeartbeatService extends BasePeriodicService {
5
+ private readonly publishHeartbeat;
6
+ readonly id: DaemonServiceId;
7
+ readonly intervalMs = 60000;
8
+ readonly deadlineMs = 30000;
9
+ constructor(publishHeartbeat: () => void);
10
+ protected onStart(_ctx: DaemonContext): Promise<void>;
11
+ protected onStop(): Promise<void>;
12
+ protected onTick(ctx: DaemonContext): Promise<void>;
13
+ }
@@ -0,0 +1,26 @@
1
+ /** Daemon heartbeat and routine-process reconciliation under supervision. */
2
+ import { monitorRunningJobs } from './runner.js';
3
+ import { reapTerminalRoutineProcesses } from '../routine-process-cleanup.js';
4
+ import { BasePeriodicService } from './service.js';
5
+ const HEARTBEAT_TICK_MS = 60_000;
6
+ const HEARTBEAT_DEADLINE_MS = 30_000;
7
+ export class HeartbeatService extends BasePeriodicService {
8
+ publishHeartbeat;
9
+ id = 'daemon-heartbeat';
10
+ intervalMs = HEARTBEAT_TICK_MS;
11
+ deadlineMs = HEARTBEAT_DEADLINE_MS;
12
+ constructor(publishHeartbeat) {
13
+ super();
14
+ this.publishHeartbeat = publishHeartbeat;
15
+ }
16
+ async onStart(_ctx) { }
17
+ async onStop() { }
18
+ async onTick(ctx) {
19
+ this.publishHeartbeat();
20
+ monitorRunningJobs();
21
+ const reaped = reapTerminalRoutineProcesses();
22
+ if (reaped.length > 0) {
23
+ ctx.log('WARN', `Reaped ${reaped.length} terminal routine process group(s): ${reaped.join(', ')}`);
24
+ }
25
+ }
26
+ }
@@ -2,21 +2,25 @@
2
2
  * MonitorEngine lifecycle as a `DaemonService` (RUSH-3193 P2).
3
3
  *
4
4
  * Wraps the `MonitorEngine` (event-triggered watchers) under the
5
- * `ServiceSupervisor` contract. This is a lifecycle-only service — the engine
6
- * runs its own internal event loop; the supervisor just calls `start()` at
7
- * boot and `stop()` at shutdown.
5
+ * `ServiceSupervisor` contract. The engine can own a timer when embedded by a
6
+ * foreground caller, but the daemon starts it in external-scheduler mode so
7
+ * every evaluation cycle receives the supervisor's deadline, health, and
8
+ * circuit-breaker semantics.
8
9
  *
9
10
  * `getEngine()` exposes the underlying `MonitorEngine` so `daemon.ts`'s
10
11
  * SIGHUP handler can call `engine.reload()` when needed.
11
12
  */
12
- import { BaseDaemonService, type DaemonContext } from './service.js';
13
+ import { BasePeriodicService, type DaemonContext } from './service.js';
13
14
  import type { DaemonServiceId } from '../daemon-services.js';
14
15
  import { MonitorEngine } from '../monitors/engine.js';
15
- export declare class MonitorEngineService extends BaseDaemonService {
16
+ export declare class MonitorEngineService extends BasePeriodicService {
16
17
  readonly id: DaemonServiceId;
18
+ readonly intervalMs = 5000;
19
+ readonly deadlineMs: number;
17
20
  private engine;
18
21
  /** Returns the live engine after `start()`, or `null` before/after. */
19
22
  getEngine(): MonitorEngine | null;
20
23
  protected onStart(ctx: DaemonContext): Promise<void>;
21
24
  protected onStop(): Promise<void>;
25
+ protected onTick(_ctx: DaemonContext): Promise<void>;
22
26
  }
@@ -2,17 +2,20 @@
2
2
  * MonitorEngine lifecycle as a `DaemonService` (RUSH-3193 P2).
3
3
  *
4
4
  * Wraps the `MonitorEngine` (event-triggered watchers) under the
5
- * `ServiceSupervisor` contract. This is a lifecycle-only service — the engine
6
- * runs its own internal event loop; the supervisor just calls `start()` at
7
- * boot and `stop()` at shutdown.
5
+ * `ServiceSupervisor` contract. The engine can own a timer when embedded by a
6
+ * foreground caller, but the daemon starts it in external-scheduler mode so
7
+ * every evaluation cycle receives the supervisor's deadline, health, and
8
+ * circuit-breaker semantics.
8
9
  *
9
10
  * `getEngine()` exposes the underlying `MonitorEngine` so `daemon.ts`'s
10
11
  * SIGHUP handler can call `engine.reload()` when needed.
11
12
  */
12
- import { BaseDaemonService } from './service.js';
13
- import { MonitorEngine } from '../monitors/engine.js';
14
- export class MonitorEngineService extends BaseDaemonService {
13
+ import { BasePeriodicService } from './service.js';
14
+ import { MONITOR_ENGINE_TICK_MS, MonitorEngine } from '../monitors/engine.js';
15
+ export class MonitorEngineService extends BasePeriodicService {
15
16
  id = 'monitors';
17
+ intervalMs = MONITOR_ENGINE_TICK_MS;
18
+ deadlineMs = 2 * 60_000;
16
19
  engine = null;
17
20
  /** Returns the live engine after `start()`, or `null` before/after. */
18
21
  getEngine() {
@@ -20,10 +23,15 @@ export class MonitorEngineService extends BaseDaemonService {
20
23
  }
21
24
  async onStart(ctx) {
22
25
  this.engine = new MonitorEngine((level, message) => ctx.log(level, message));
23
- this.engine.start();
26
+ this.engine.start({ externalScheduler: true });
24
27
  }
25
28
  async onStop() {
26
29
  this.engine?.stop();
27
30
  this.engine = null;
28
31
  }
32
+ async onTick(_ctx) {
33
+ if (!this.engine)
34
+ throw new Error('monitor engine tick requested before start');
35
+ await this.engine.tick();
36
+ }
29
37
  }
@@ -233,6 +233,21 @@ export declare function inferFinalStatusFromLog(stdoutPath: string, agent: Agent
233
233
  * `host:`-placed runs have no local pid and are excluded.
234
234
  */
235
235
  export declare function listLiveRoutineChildren(): number[];
236
+ /**
237
+ * Whether a run record marked `running` is genuinely in flight right now. A
238
+ * `running` status alone is not enough: `writeActiveClaim` stamps a provisional
239
+ * claim as `running` with `pid: null` BEFORE the child spawns, and
240
+ * `monitorRunningJobs()` does not reap a null-pid record (it has no process to
241
+ * probe, and ages it out only past the configured timeout — up to a week). So a
242
+ * daemon crash in that spawn window leaves a `running` record for a run that is
243
+ * not running (RUSH-2640). A caller answering "is one running now?" MUST gate on
244
+ * this, mirroring `listLiveRoutineChildren`'s local-liveness test:
245
+ * - a local child is in flight only while its pid is still ours (alive, same
246
+ * birth time — a dead-and-reused pid does not count);
247
+ * - a `host:`-placed run has no local pid by design and is in flight until its
248
+ * remote `.exit` is reconciled (`finalizeHostRun`).
249
+ */
250
+ export declare function isRunGenuinelyInFlight(meta: RunMeta): boolean;
236
251
  /** Scan all runs marked "running" and finalize any whose process has exited. */
237
252
  export declare function monitorRunningJobs(): void;
238
253
  export {};
@@ -2263,6 +2263,29 @@ export function listLiveRoutineChildren() {
2263
2263
  }
2264
2264
  return pids;
2265
2265
  }
2266
+ /**
2267
+ * Whether a run record marked `running` is genuinely in flight right now. A
2268
+ * `running` status alone is not enough: `writeActiveClaim` stamps a provisional
2269
+ * claim as `running` with `pid: null` BEFORE the child spawns, and
2270
+ * `monitorRunningJobs()` does not reap a null-pid record (it has no process to
2271
+ * probe, and ages it out only past the configured timeout — up to a week). So a
2272
+ * daemon crash in that spawn window leaves a `running` record for a run that is
2273
+ * not running (RUSH-2640). A caller answering "is one running now?" MUST gate on
2274
+ * this, mirroring `listLiveRoutineChildren`'s local-liveness test:
2275
+ * - a local child is in flight only while its pid is still ours (alive, same
2276
+ * birth time — a dead-and-reused pid does not count);
2277
+ * - a `host:`-placed run has no local pid by design and is in flight until its
2278
+ * remote `.exit` is reconciled (`finalizeHostRun`).
2279
+ */
2280
+ export function isRunGenuinelyInFlight(meta) {
2281
+ if (meta.status !== 'running')
2282
+ return false;
2283
+ if (meta.hostTaskId)
2284
+ return true;
2285
+ if (!meta.pid)
2286
+ return false;
2287
+ return isPidOurs(meta.pid, meta.spawnedAt);
2288
+ }
2266
2289
  /** Scan all runs marked "running" and finalize any whose process has exited. */
2267
2290
  export function monitorRunningJobs() {
2268
2291
  const runsDir = getRunsDir();
@@ -8,13 +8,14 @@
8
8
  * over hosting whenever the daemon is NOT already hosting AND no healthy
9
9
  * standalone broker answers a ping.
10
10
  */
11
- import { BaseDaemonService, type DaemonContext } from './service.js';
11
+ import { BasePeriodicService, type DaemonContext } from './service.js';
12
12
  import type { DaemonServiceId } from '../daemon-services.js';
13
- export declare class SecretsBrokerService extends BaseDaemonService {
13
+ export declare class SecretsBrokerService extends BasePeriodicService {
14
14
  readonly id: DaemonServiceId;
15
+ readonly intervalMs = 60000;
16
+ readonly deadlineMs = 30000;
15
17
  private hostedBroker;
16
- private selfHealTimer;
17
- private selfHealInFlight;
18
18
  protected onStart(ctx: DaemonContext): Promise<void>;
19
19
  protected onStop(): Promise<void>;
20
+ protected onTick(ctx: DaemonContext): Promise<void>;
20
21
  }