@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (317) hide show
  1. package/README.md +6 -4
  2. package/README.zh-TW.md +6 -4
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +4 -1
  6. package/dist/agent-endpoint.js +82 -14
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-token-header.d.ts +13 -0
  9. package/dist/agent-token-header.js +16 -0
  10. package/dist/agent-token-header.js.map +1 -0
  11. package/dist/atomic-write.d.ts +21 -0
  12. package/dist/atomic-write.js +65 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/auth-api.js +2 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/codex.d.ts +7 -0
  19. package/dist/backend/codex.js +7 -0
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/grok.js +1 -1
  22. package/dist/backend/grok.js.map +1 -1
  23. package/dist/backend/kiro.d.ts +7 -0
  24. package/dist/backend/kiro.js +82 -33
  25. package/dist/backend/kiro.js.map +1 -1
  26. package/dist/backend/muse.d.ts +7 -0
  27. package/dist/backend/muse.js +8 -1
  28. package/dist/backend/muse.js.map +1 -1
  29. package/dist/backend/types.d.ts +12 -0
  30. package/dist/backend/types.js.map +1 -1
  31. package/dist/binary-probe.d.ts +80 -0
  32. package/dist/binary-probe.js +182 -0
  33. package/dist/binary-probe.js.map +1 -0
  34. package/dist/cache-analysis.d.ts +92 -0
  35. package/dist/cache-analysis.js +147 -0
  36. package/dist/cache-analysis.js.map +1 -0
  37. package/dist/cache-ledger.d.ts +108 -0
  38. package/dist/cache-ledger.js +230 -0
  39. package/dist/cache-ledger.js.map +1 -0
  40. package/dist/cache-prices.d.ts +32 -0
  41. package/dist/cache-prices.js +73 -0
  42. package/dist/cache-prices.js.map +1 -0
  43. package/dist/cache-scan.d.ts +39 -0
  44. package/dist/cache-scan.js +115 -0
  45. package/dist/cache-scan.js.map +1 -0
  46. package/dist/cache-service.d.ts +131 -0
  47. package/dist/cache-service.js +452 -0
  48. package/dist/cache-service.js.map +1 -0
  49. package/dist/channel/adapters/discord.d.ts +5 -1
  50. package/dist/channel/adapters/discord.js +56 -11
  51. package/dist/channel/adapters/discord.js.map +1 -1
  52. package/dist/channel/adapters/telegram.d.ts +38 -1
  53. package/dist/channel/adapters/telegram.js +82 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +1 -1
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.d.ts +8 -1
  58. package/dist/channel/tool-router.js +4 -2
  59. package/dist/channel/tool-router.js.map +1 -1
  60. package/dist/channel/types.d.ts +21 -0
  61. package/dist/cli-entry.d.ts +19 -0
  62. package/dist/cli-entry.js +36 -0
  63. package/dist/cli-entry.js.map +1 -0
  64. package/dist/cli.js +376 -54
  65. package/dist/cli.js.map +1 -1
  66. package/dist/connection-secrets.d.ts +5 -0
  67. package/dist/connection-secrets.js.map +1 -1
  68. package/dist/cpu-profile.js +50 -0
  69. package/dist/cpu-profile.js.map +1 -1
  70. package/dist/daemon.d.ts +18 -0
  71. package/dist/daemon.js +256 -63
  72. package/dist/daemon.js.map +1 -1
  73. package/dist/delivery-outbox.d.ts +1 -0
  74. package/dist/delivery-outbox.js +12 -0
  75. package/dist/delivery-outbox.js.map +1 -1
  76. package/dist/discord-permissions.d.ts +33 -0
  77. package/dist/discord-permissions.js +42 -0
  78. package/dist/discord-permissions.js.map +1 -0
  79. package/dist/env-file.d.ts +11 -0
  80. package/dist/env-file.js +27 -0
  81. package/dist/env-file.js.map +1 -0
  82. package/dist/event-log.d.ts +4 -1
  83. package/dist/event-log.js +6 -3
  84. package/dist/event-log.js.map +1 -1
  85. package/dist/fleet-context.d.ts +5 -0
  86. package/dist/fleet-context.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +5 -0
  88. package/dist/fleet-control-audit.js +7 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-level-config.d.ts +8 -1
  91. package/dist/fleet-level-config.js +53 -0
  92. package/dist/fleet-level-config.js.map +1 -1
  93. package/dist/fleet-manager.d.ts +122 -7
  94. package/dist/fleet-manager.js +871 -152
  95. package/dist/fleet-manager.js.map +1 -1
  96. package/dist/fleet-stop-budget.d.ts +22 -0
  97. package/dist/fleet-stop-budget.js +95 -0
  98. package/dist/fleet-stop-budget.js.map +1 -0
  99. package/dist/full-restart.js +2 -3
  100. package/dist/full-restart.js.map +1 -1
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  102. package/dist/install-lock.d.ts +49 -0
  103. package/dist/install-lock.js +200 -0
  104. package/dist/install-lock.js.map +1 -0
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/kiro-transcript-lane.d.ts +8 -4
  108. package/dist/kiro-transcript-lane.js +66 -44
  109. package/dist/kiro-transcript-lane.js.map +1 -1
  110. package/dist/locale.js +96 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/next-steps.d.ts +5 -0
  113. package/dist/next-steps.js +26 -0
  114. package/dist/next-steps.js.map +1 -0
  115. package/dist/outbound-handlers.js +8 -0
  116. package/dist/outbound-handlers.js.map +1 -1
  117. package/dist/outbound-schemas.d.ts +4 -0
  118. package/dist/outbound-schemas.js +7 -2
  119. package/dist/outbound-schemas.js.map +1 -1
  120. package/dist/outbox-open-error.d.ts +7 -0
  121. package/dist/outbox-open-error.js +15 -0
  122. package/dist/outbox-open-error.js.map +1 -0
  123. package/dist/package-preimage.d.ts +26 -0
  124. package/dist/package-preimage.js +96 -0
  125. package/dist/package-preimage.js.map +1 -0
  126. package/dist/pane-context-cache.d.ts +25 -0
  127. package/dist/pane-context-cache.js +73 -0
  128. package/dist/pane-context-cache.js.map +1 -0
  129. package/dist/paths.d.ts +0 -7
  130. package/dist/paths.js +8 -2
  131. package/dist/paths.js.map +1 -1
  132. package/dist/provider-probe.d.ts +15 -0
  133. package/dist/provider-probe.js +20 -0
  134. package/dist/provider-probe.js.map +1 -1
  135. package/dist/public-link-progress.d.ts +73 -0
  136. package/dist/public-link-progress.js +190 -0
  137. package/dist/public-link-progress.js.map +1 -0
  138. package/dist/public-web-gateway.js +2 -2
  139. package/dist/public-web-gateway.js.map +1 -1
  140. package/dist/public-web-link.d.ts +6 -1
  141. package/dist/public-web-link.js +87 -15
  142. package/dist/public-web-link.js.map +1 -1
  143. package/dist/quickstart-api.d.ts +93 -20
  144. package/dist/quickstart-api.js +194 -62
  145. package/dist/quickstart-api.js.map +1 -1
  146. package/dist/quickstart.d.ts +15 -0
  147. package/dist/quickstart.js +110 -33
  148. package/dist/quickstart.js.map +1 -1
  149. package/dist/reply-buttons-controller.d.ts +98 -0
  150. package/dist/reply-buttons-controller.js +174 -0
  151. package/dist/reply-buttons-controller.js.map +1 -0
  152. package/dist/reply-buttons.d.ts +113 -0
  153. package/dist/reply-buttons.js +208 -0
  154. package/dist/reply-buttons.js.map +1 -0
  155. package/dist/reply-dedup.js +3 -1
  156. package/dist/reply-dedup.js.map +1 -1
  157. package/dist/restart-guard.d.ts +53 -0
  158. package/dist/restart-guard.js +114 -0
  159. package/dist/restart-guard.js.map +1 -0
  160. package/dist/scheduler/db.js +6 -2
  161. package/dist/scheduler/db.js.map +1 -1
  162. package/dist/service-activation.d.ts +62 -7
  163. package/dist/service-activation.js +237 -55
  164. package/dist/service-activation.js.map +1 -1
  165. package/dist/service-installer.d.ts +56 -1
  166. package/dist/service-installer.js +152 -12
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-plan.d.ts +65 -0
  169. package/dist/service-plan.js +108 -0
  170. package/dist/service-plan.js.map +1 -0
  171. package/dist/settings-api.js +92 -10
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/settings-authority.d.ts +11 -0
  174. package/dist/settings-authority.js +128 -0
  175. package/dist/settings-authority.js.map +1 -0
  176. package/dist/settings-change.d.ts +5 -0
  177. package/dist/settings-change.js +37 -8
  178. package/dist/settings-change.js.map +1 -1
  179. package/dist/settings-confirmation.d.ts +3 -0
  180. package/dist/settings-confirmation.js +7 -1
  181. package/dist/settings-confirmation.js.map +1 -1
  182. package/dist/settings-effect.js +19 -7
  183. package/dist/settings-effect.js.map +1 -1
  184. package/dist/settings-http-confirmation.js +1 -1
  185. package/dist/settings-http-confirmation.js.map +1 -1
  186. package/dist/settings-redaction.d.ts +18 -0
  187. package/dist/settings-redaction.js +58 -0
  188. package/dist/settings-redaction.js.map +1 -0
  189. package/dist/setup-form.d.ts +9 -1
  190. package/dist/setup-form.js +25 -1
  191. package/dist/setup-form.js.map +1 -1
  192. package/dist/setup-host.js +15 -6
  193. package/dist/setup-host.js.map +1 -1
  194. package/dist/setup-wizard.js +27 -19
  195. package/dist/setup-wizard.js.map +1 -1
  196. package/dist/sqlite-open-errors.d.ts +12 -0
  197. package/dist/sqlite-open-errors.js +13 -0
  198. package/dist/sqlite-open-errors.js.map +1 -0
  199. package/dist/systemd-runtime.d.ts +15 -0
  200. package/dist/systemd-runtime.js +41 -0
  201. package/dist/systemd-runtime.js.map +1 -0
  202. package/dist/tmux-control.d.ts +44 -0
  203. package/dist/tmux-control.js +170 -21
  204. package/dist/tmux-control.js.map +1 -1
  205. package/dist/token-env-name.d.ts +37 -0
  206. package/dist/token-env-name.js +93 -0
  207. package/dist/token-env-name.js.map +1 -0
  208. package/dist/tool-permissions.js +5 -3
  209. package/dist/tool-permissions.js.map +1 -1
  210. package/dist/topic-commands.d.ts +7 -16
  211. package/dist/topic-commands.js +56 -85
  212. package/dist/topic-commands.js.map +1 -1
  213. package/dist/transcript-monitor.js +3 -0
  214. package/dist/transcript-monitor.js.map +1 -1
  215. package/dist/transcript-sources.d.ts +68 -0
  216. package/dist/transcript-sources.js +336 -29
  217. package/dist/transcript-sources.js.map +1 -1
  218. package/dist/transcript-turns.d.ts +39 -0
  219. package/dist/transcript-turns.js +58 -0
  220. package/dist/transcript-turns.js.map +1 -0
  221. package/dist/tunnel/cloudflared-install.d.ts +18 -0
  222. package/dist/tunnel/cloudflared-install.js +15 -1
  223. package/dist/tunnel/cloudflared-install.js.map +1 -1
  224. package/dist/turn-reply-guard.d.ts +3 -0
  225. package/dist/turn-reply-guard.js.map +1 -1
  226. package/dist/ui/chat-commands.js +43 -0
  227. package/dist/ui/chat-render.js +34 -8
  228. package/dist/ui/chat-store.js +51 -2
  229. package/dist/ui/chat-strings.js +30 -2
  230. package/dist/ui/chat-thread.js +141 -47
  231. package/dist/ui/first-run.js +64 -0
  232. package/dist/ui/fleet-cache.js +168 -0
  233. package/dist/ui/html-attachment.js +26 -0
  234. package/dist/ui/panel-chat.js +117 -23
  235. package/dist/ui/panel-fleet.js +31 -17
  236. package/dist/ui/panel-settings.js +29 -15
  237. package/dist/ui/preview-panel.js +237 -0
  238. package/dist/ui/preview.js +7 -3
  239. package/dist/ui/settings-apply.js +3 -1
  240. package/dist/ui/settings-dialogs.js +95 -25
  241. package/dist/ui/settings-discord.js +130 -0
  242. package/dist/ui/settings-model.js +21 -0
  243. package/dist/ui/settings-strings.js +50 -8
  244. package/dist/ui/settings-token.js +54 -0
  245. package/dist/ui/settings-wizard.js +63 -29
  246. package/dist/ui/shared/app-route.js +1 -1
  247. package/dist/ui/shared/app-shell.js +25 -37
  248. package/dist/ui/shared/app-store.js +5 -3
  249. package/dist/ui/shared/app-stream.js +2 -1
  250. package/dist/ui/shared/app.css +181 -5
  251. package/dist/ui/shared/app.js +2 -1
  252. package/dist/ui/shared/instance-nav.js +310 -0
  253. package/dist/ui/shared/panel-view.js +12 -190
  254. package/dist/ui/shared/signin.js +13 -1
  255. package/dist/ui/shared/ui-icons.js +2 -0
  256. package/dist/ui/shared/view-roster-store.js +5 -0
  257. package/dist/ui/shared/view-strings.js +12 -0
  258. package/dist/ui/signin.html +2 -1
  259. package/dist/update-dispatch.d.ts +28 -0
  260. package/dist/update-dispatch.js +73 -0
  261. package/dist/update-dispatch.js.map +1 -0
  262. package/dist/update-install.d.ts +78 -8
  263. package/dist/update-install.js +158 -30
  264. package/dist/update-install.js.map +1 -1
  265. package/dist/update-launch.d.ts +22 -0
  266. package/dist/update-launch.js +106 -0
  267. package/dist/update-launch.js.map +1 -0
  268. package/dist/update-marker.js +3 -8
  269. package/dist/update-marker.js.map +1 -1
  270. package/dist/usage/format-rich.js +7 -1
  271. package/dist/usage/format-rich.js.map +1 -1
  272. package/dist/usage/i18n-keys.d.ts +1 -1
  273. package/dist/usage/i18n-keys.js +1 -1
  274. package/dist/usage/i18n-keys.js.map +1 -1
  275. package/dist/usage/providers.d.ts +4 -0
  276. package/dist/usage/providers.js +2 -1
  277. package/dist/usage/providers.js.map +1 -1
  278. package/dist/usage/usage-api.js +48 -2
  279. package/dist/usage/usage-api.js.map +1 -1
  280. package/dist/view-api.d.ts +40 -0
  281. package/dist/view-api.js +28 -10
  282. package/dist/view-api.js.map +1 -1
  283. package/dist/web-api.d.ts +22 -0
  284. package/dist/web-api.js +115 -16
  285. package/dist/web-api.js.map +1 -1
  286. package/dist/web-auth.d.ts +8 -0
  287. package/dist/web-auth.js +34 -8
  288. package/dist/web-auth.js.map +1 -1
  289. package/dist/web-chat-history.d.ts +21 -0
  290. package/dist/web-chat-history.js +44 -0
  291. package/dist/web-chat-history.js.map +1 -1
  292. package/dist/web-commands.d.ts +77 -0
  293. package/dist/web-commands.js +81 -0
  294. package/dist/web-commands.js.map +1 -0
  295. package/dist/web-host-guard.d.ts +18 -2
  296. package/dist/web-host-guard.js +30 -5
  297. package/dist/web-host-guard.js.map +1 -1
  298. package/dist/web-session.d.ts +7 -1
  299. package/dist/web-session.js +23 -2
  300. package/dist/web-session.js.map +1 -1
  301. package/dist/web-shell-routes.d.ts +1 -1
  302. package/dist/web-shell-routes.js +1 -1
  303. package/dist/web-shell-routes.js.map +1 -1
  304. package/launcher/agend +110 -0
  305. package/launcher/agend-agent +110 -0
  306. package/launcher/agend-agent.cjs +4 -0
  307. package/launcher/agend.cjs +4 -0
  308. package/launcher/install-admission.cjs +105 -0
  309. package/launcher/launch.cjs +64 -0
  310. package/launcher/postinstall.cjs +118 -0
  311. package/launcher/run-in-process.cjs +13 -0
  312. package/launcher/runtime-platform.cjs +86 -0
  313. package/launcher/runtime-select.cjs +217 -0
  314. package/package.json +12 -4
  315. package/scripts/preinstall-guard.cjs +40 -21
  316. package/templates/launchd.plist.ejs +2 -2
  317. package/templates/systemd.service.ejs +4 -4
package/dist/cli.js CHANGED
@@ -19,13 +19,14 @@ import { Cron } from "croner";
19
19
  import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, realpathSync, } from "node:fs";
20
20
  import { homedir, totalmem, freemem, userInfo } from "node:os";
21
21
  import { fileURLToPath } from "node:url";
22
- import { spawn, execSync, execFileSync } from "node:child_process";
22
+ import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
23
23
  import { getAgendHome, getTmuxSocketName } from "./paths.js";
24
24
  import { hasPausedMarker } from "./pause-marker.js";
25
25
  import { COMPLETION_SHELLS, completionScript } from "./completion.js";
26
26
  import { getUpdateSelector, isExactVersion, isUnrequestedDowngrade, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, runningFleetPredatesInstall, processStartMs, } from "./update-check.js";
27
27
  import { clearUpdateMarker, markUpdateInProgress, setUpdateProgressStage } from "./update-marker.js";
28
- import { describeSignalSource, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
28
+ import { describeSignalSource, forceAllowed, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
29
+ import { FLEET_STOP_TIMEOUT_MS, stopDetachedOwner, systemdStopTimeoutMs, detachedProcessStart, detachedProcessExited } from "./fleet-stop-budget.js";
29
30
  import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
30
31
  import { limitFleetCoreDumps } from "./coredump-filter.js";
31
32
  import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
@@ -36,6 +37,7 @@ import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
36
37
  import { DeliveryStatusArgs } from "./outbound-schemas.js";
37
38
  import { requestCpuProfile } from "./profile-control.js";
38
39
  import { requestSettingsConfirmation } from "./settings-control.js";
40
+ import { canonicalCliEntry, selfCommand } from "./cli-entry.js";
39
41
  /** Prefix tmux args with -L when socket isolation is active. */
40
42
  function tmuxArgs(args) {
41
43
  const socket = getTmuxSocketName();
@@ -44,6 +46,39 @@ function tmuxArgs(args) {
44
46
  const __filename = fileURLToPath(import.meta.url);
45
47
  const __dirname = dirname(__filename);
46
48
  const DATA_DIR = getAgendHome();
49
+ /** Real-filesystem, real-process deps for the launchd plan (service-plan.ts) and its activation. */
50
+ function planDeps() {
51
+ return {
52
+ run: (command, args) => {
53
+ const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
54
+ return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
55
+ },
56
+ readFile: (path) => { try {
57
+ return readFileSync(path, "utf-8");
58
+ }
59
+ catch {
60
+ return null;
61
+ } },
62
+ writeFile: (path, content) => writeFileSync(path, content),
63
+ removeFile: (path) => { try {
64
+ unlinkSync(path);
65
+ }
66
+ catch { /* absent */ } },
67
+ realpath: (path) => { try {
68
+ return realpathSync(path);
69
+ }
70
+ catch {
71
+ return null;
72
+ } },
73
+ isExecutable: (path) => { try {
74
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
75
+ }
76
+ catch {
77
+ return false;
78
+ } },
79
+ now: () => new Date(),
80
+ };
81
+ }
47
82
  const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
48
83
  // CLI commands run outside FleetManager. Honor an explicit fleet locale while
49
84
  // keeping the historical English CLI default when config is missing/unset.
@@ -1007,13 +1042,20 @@ backend
1007
1042
  const svcPath = join(homedir(), ".config/systemd/user/com.agend.fleet.service");
1008
1043
  if (existsSync(svcPath)) {
1009
1044
  const svc = readFileSync(svcPath, "utf-8");
1010
- const match = svc.match(/ExecStart=(\S+)/);
1011
- const svcBin = match?.[1] ?? "";
1012
- const currentBin = es("which agend", { stdio: "pipe" }).toString().trim();
1013
- if (svcBin === currentBin)
1045
+ // The CLI the unit starts — `<entry> fleet start`, or `<node> <entry> fleet start` — against this CLI's own
1046
+ // canonical entry (#1450: `agend` on PATH is the sh launcher, never what a unit records).
1047
+ const { unitCliEntry } = await import("./service-installer.js");
1048
+ const svcEntry = unitCliEntry(svc);
1049
+ let svcReal = svcEntry;
1050
+ try {
1051
+ svcReal = realpathSync(svcEntry);
1052
+ }
1053
+ catch { /* reported as is */ }
1054
+ const current = canonicalCliEntry();
1055
+ if (svcReal === current)
1014
1056
  ok(`service ExecStart${" ".repeat(3)} matches current binary`);
1015
1057
  else
1016
- fail(`service ExecStart${" ".repeat(3)} ${svcBin} ≠ ${currentBin}`);
1058
+ fail(`service ExecStart${" ".repeat(3)} ${svcEntry || "(none)"} ≠ ${current}`);
1017
1059
  // Check Restart=on-failure
1018
1060
  if (svc.includes("Restart=on-failure")) {
1019
1061
  ok(`service restart${" ".repeat(5)} Restart=on-failure configured`);
@@ -1438,6 +1480,9 @@ program
1438
1480
  }
1439
1481
  process.exitCode = 1;
1440
1482
  }
1483
+ if (restartOutcome === "pending")
1484
+ process.exitCode = 75;
1485
+ return restartOutcome;
1441
1486
  };
1442
1487
  const nvmSh = join(homedir(), ".nvm", "nvm.sh");
1443
1488
  /**
@@ -1447,7 +1492,9 @@ program
1447
1492
  * (process.argv[1] may be another checkout). A failed target check leaves the running fleet alone.
1448
1493
  */
1449
1494
  const activateVerified = async (verified, viaNvm) => {
1450
- const { newAgendInvocation } = await import("./update-install.js");
1495
+ const { newAgendInvocation, retireSystemCopy, activationSettled } = await import("./update-install.js");
1496
+ // What the restart came to — carried, never inferred from the exit code (pending ≠ restarted).
1497
+ let restartResult = null;
1451
1498
  const { activateService } = await import("./service-activation.js");
1452
1499
  const { getServicePath, getSystemServicePath, detectPlatform } = await import("./service-installer.js");
1453
1500
  const { accessSync, constants } = await import("node:fs");
@@ -1455,10 +1502,18 @@ program
1455
1502
  // The authoritative manager, chosen as `agend restart` chooses: system unit > user unit / launchd plist > none.
1456
1503
  const systemUnit = getSystemServicePath();
1457
1504
  const ownService = systemUnit ? null : getServicePath();
1458
- const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false }
1505
+ const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false, unitPath: systemUnit }
1459
1506
  : ownService && detectPlatform() === "macos" ? { kind: "launchd", label: "com.agend.fleet", plistPath: ownService, domain: `gui/${process.getuid?.() ?? 501}` }
1460
- : ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true }
1507
+ : ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true, unitPath: ownService }
1461
1508
  : { kind: "detached" };
1509
+ // C6: a failure before the fleet runs the new install puts the previous package back too (taken before npm).
1510
+ const { restorePackagePreimage, prunePreimages } = await import("./package-preimage.js");
1511
+ const rollback = verified.rollback;
1512
+ const restoreChecked = rollback?.preimage ? () => {
1513
+ const back = restorePackagePreimage(rollback.root, rollback.prefix, rollback.preimage);
1514
+ return { ok: back.ok, message: back.ok ? `The previous package (v${rollback.preimage.version}) is back in place` : `The previous package could NOT be put back: ${back.reason}` };
1515
+ } : undefined;
1516
+ const restorePackage = restoreChecked ? () => restoreChecked().message : undefined;
1462
1517
  const capture = (command, args) => {
1463
1518
  const result = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
1464
1519
  return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
@@ -1505,16 +1560,47 @@ program
1505
1560
  spawnSync(newAgend.command, [...newAgend.args, "completion", "install", "--refresh"], { encoding: "utf-8", timeout: 15_000, stdio: "ignore" });
1506
1561
  }
1507
1562
  catch { /* cosmetic */ }
1508
- restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
1563
+ restartResult = restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
1564
+ return restartResult;
1509
1565
  },
1510
1566
  log: message => console.log(message),
1567
+ restorePackage,
1568
+ systemdRecovery: restoreChecked ? { restorePackage: restoreChecked } : undefined,
1511
1569
  });
1570
+ // Settled: after a success only this transition's preimage is kept (for a repair); a failure consumed it.
1571
+ if (activationSettled(outcome, restartResult) && rollback)
1572
+ prunePreimages(rollback.prefix, rollback.preimage);
1573
+ // nvm transition: the old system copy is what the previous service ran. Only once the fleet POSITIVELY runs the
1574
+ // new install (activationSettled) is it removed; pending, failed or refused, it stays, so the old unit still starts.
1575
+ if (verified.retireSystemCopy) {
1576
+ if (activationSettled(outcome, restartResult)) {
1577
+ const retireResult = retireSystemCopy({ run: (command, args) => capture(command, args), log: message => console.log(message) }, verified.npmPath);
1578
+ if (!retireResult.ok) {
1579
+ console.error(` ✗ Old system install was not removed: ${retireResult.reason}`);
1580
+ process.exitCode = 1;
1581
+ }
1582
+ }
1583
+ else {
1584
+ console.log(" Note: the old system install was kept, since the new one is not running yet.");
1585
+ }
1586
+ }
1587
+ // The refresh above (`install --no-activate`) records a launchd plan; this activation consumed it, whatever it
1588
+ // came to — never leave one for a later `agend restart` to act on.
1589
+ if (manager.kind === "launchd") {
1590
+ const { planPath } = await import("./service-plan.js");
1591
+ planDeps().removeFile(planPath(DATA_DIR));
1592
+ }
1512
1593
  if (outcome.ok && outcome.via === "launchd-activation") {
1513
1594
  console.log(" ✓ Service restarted (launchd loaded the verified job)");
1514
1595
  // The new fleet clears the update marker once it is up.
1515
1596
  return;
1516
1597
  }
1517
1598
  if (!outcome.ok) {
1599
+ if (outcome.pending) {
1600
+ console.log(outcome.message);
1601
+ process.exitCode = 75;
1602
+ return;
1603
+ }
1518
1604
  console.error(outcome.message);
1519
1605
  if (!setUpdateProgressStage(DATA_DIR, "failed", { error: outcome.message.trim() }))
1520
1606
  clearUpdateMarker(DATA_DIR);
@@ -1625,8 +1711,34 @@ program
1625
1711
  console.log(" Using nvm to install Node 22...");
1626
1712
  }
1627
1713
  // ── Install, then verify, then clean up (#1446): nothing is removed before the new install is proven ──
1714
+ // #1450 C6: npm replaces THIS package in place. Everything the rest of the update runs — verification, rollback,
1715
+ // service work — is loaded now, before npm; a later import would load the NEW package's files into this process.
1716
+ await Promise.all([
1717
+ import("./service-activation.js"), import("./service-installer.js"), import("./service-plan.js"),
1718
+ import("./restart-guard.js"), import("./package-preimage.js"), import("./install-lock.js"), import("./update-check.js"),
1719
+ ]);
1628
1720
  const { runUpdateInstall } = await import("./update-install.js");
1629
- const installed = runUpdateInstall({ pkg, targetVersion, viaNvm: needsSudo, nvmSh }, {
1721
+ // #1450 C1: this update owns the npm prefix from before npm runs until it settles (verify, service, restart) —
1722
+ // released when this process exits, however it exits; a crash leaves a lock the next update reclaims.
1723
+ const { acquireInstallLock } = await import("./install-lock.js");
1724
+ const { randomBytes } = await import("node:crypto");
1725
+ const { createRequire } = await import("node:module");
1726
+ const admission = createRequire(import.meta.url)("../launcher/install-admission.cjs");
1727
+ let releaseLock = () => { };
1728
+ process.once("exit", () => releaseLock());
1729
+ const installed = runUpdateInstall({
1730
+ pkg, targetVersion, viaNvm: needsSudo, nvmSh, rollback: true,
1731
+ lock: prefix => {
1732
+ const lock = acquireInstallLock(prefix, { spec: pkg, agendHome: DATA_DIR }, {
1733
+ pid: process.pid, processStart: admission.processStart, newToken: () => randomBytes(16).toString("hex"),
1734
+ now: () => new Date(), log: message => console.log(message),
1735
+ });
1736
+ if (!lock.ok)
1737
+ return lock;
1738
+ releaseLock = lock.release;
1739
+ return { ok: true, token: lock.token };
1740
+ },
1741
+ }, {
1630
1742
  run: (command, args, options = {}) => {
1631
1743
  if (options.inherit)
1632
1744
  console.log("");
@@ -1634,6 +1746,7 @@ program
1634
1746
  encoding: "utf-8",
1635
1747
  timeout: options.timeoutMs,
1636
1748
  stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
1749
+ ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
1637
1750
  });
1638
1751
  return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1639
1752
  },
@@ -1673,15 +1786,45 @@ program
1673
1786
  .option("--activate", "Deprecated; service activation is now the default")
1674
1787
  .option("--no-activate", "Write the service file without activating it")
1675
1788
  .action(async (opts) => {
1676
- const { installService, activateService, detectPlatform } = await import("./service-installer.js");
1677
- const execPath = process.argv[1];
1678
- const svcPath = installService({
1789
+ const { installService, activateService, detectPlatform, renderLaunchdPlist, launchdPlistPath } = await import("./service-installer.js");
1790
+ const execPath = canonicalCliEntry();
1791
+ const serviceVars = {
1679
1792
  label: "com.agend.fleet",
1680
1793
  execPath,
1681
1794
  path: process.env.PATH,
1682
1795
  workingDirectory: DATA_DIR,
1683
1796
  logPath: join(DATA_DIR, "fleet.log"),
1684
- });
1797
+ };
1798
+ // #1450 C6: on launchd, writing a new plist without activating is a PLANNED activation — proven, recorded, and
1799
+ // left for `agend restart` (a reload there would itself start the job). See service-plan.ts.
1800
+ if (opts.activate === false && detectPlatform() === "macos") {
1801
+ const { refreshLaunchdWithoutActivating, planPath } = await import("./service-plan.js");
1802
+ // A refresh revokes any earlier plan first, whatever it comes to (a failed refresh leaves nothing to activate).
1803
+ planDeps().removeFile(planPath(DATA_DIR));
1804
+ const { expectedTuple } = await import("./restart-guard.js");
1805
+ const expectation = expectedTuple();
1806
+ if (!expectation.ok) {
1807
+ console.error(` ✗ ${expectation.reason}`);
1808
+ process.exitCode = 1;
1809
+ return;
1810
+ }
1811
+ const uid = process.getuid?.() ?? 501;
1812
+ const plistPath = launchdPlistPath(serviceVars.label);
1813
+ mkdirSync(dirname(plistPath), { recursive: true });
1814
+ const outcome = refreshLaunchdWithoutActivating({
1815
+ label: serviceVars.label, target: `gui/${uid}/${serviceVars.label}`, plistPath, newPlist: renderLaunchdPlist(serviceVars),
1816
+ expected: expectation.expected, pkgDir: dirname(dirname(execPath)), agendHome: DATA_DIR,
1817
+ }, planDeps());
1818
+ if (!outcome.ok) {
1819
+ console.error(` ✗ ${outcome.message}`);
1820
+ process.exitCode = 1;
1821
+ return;
1822
+ }
1823
+ console.log(t("install.path", plistPath));
1824
+ console.log(` ${outcome.message}`);
1825
+ return;
1826
+ }
1827
+ const svcPath = installService(serviceVars);
1685
1828
  console.log(t("install.path", svcPath));
1686
1829
  if (opts.activate !== false) {
1687
1830
  const pidPath = join(DATA_DIR, "fleet.pid");
@@ -1816,7 +1959,9 @@ program
1816
1959
  if (!userServicePath && !systemServicePath) {
1817
1960
  console.log("No service installed. Starting fleet directly...");
1818
1961
  const { spawn } = await import("node:child_process");
1819
- const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
1962
+ // This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
1963
+ const start = selfCommand(["fleet", "start"]);
1964
+ const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
1820
1965
  child.unref();
1821
1966
  console.log("Fleet starting in background.");
1822
1967
  return;
@@ -1837,9 +1982,52 @@ program
1837
1982
  .command("restart")
1838
1983
  .description("Restart the AgEnD service (auto-detects systemd/launchd/detached)")
1839
1984
  .option("-y, --yes", "Confirm when run from a fleet agent session (this affects every instance)")
1985
+ .option("--force", "Restart even when the loaded service does not start this install on its selected Node (operators only)")
1840
1986
  .action(async (opts) => {
1987
+ // --force overrides the C6 guard: an operator's own decision, never a fleet agent's or a fleet-internal spawn's.
1988
+ if (opts.force && !forceAllowed()) {
1989
+ console.error(" ✗ Not restarting: --force is for an operator's own shell, not a fleet agent session or the fleet's own commands. Nothing was stopped.");
1990
+ process.exit(1);
1991
+ }
1841
1992
  if (!gateFleetControl(DATA_DIR, "restart", { yes: opts.yes }))
1842
1993
  process.exit(1);
1994
+ // #1450 C6: what the restarted fleet must run, by this package's own selection (C2) — decided before anything stops.
1995
+ const guard = await import("./restart-guard.js");
1996
+ const guardDeps = {
1997
+ realpath: (path) => { try {
1998
+ return realpathSync(path);
1999
+ }
2000
+ catch {
2001
+ return null;
2002
+ } },
2003
+ isExecutable: (path) => { try {
2004
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
2005
+ }
2006
+ catch {
2007
+ return false;
2008
+ } },
2009
+ };
2010
+ const captureRun = (command, args) => {
2011
+ const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
2012
+ return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
2013
+ };
2014
+ const expectation = guard.expectedTuple();
2015
+ /** Refuse (nothing stopped) unless the definition about to run is proven; `--force` overrides for an operator. */
2016
+ const refuses = (judged) => {
2017
+ if (judged.ok)
2018
+ return false;
2019
+ if (opts.force) {
2020
+ console.log(` ⚠ --force: ${judged.reason}; restarting anyway.`);
2021
+ return false;
2022
+ }
2023
+ console.error(` ✗ Not restarting: ${judged.reason}. Nothing was stopped.`);
2024
+ if (judged.recovery)
2025
+ console.error(` To repair: ${judged.recovery}`);
2026
+ process.exitCode = 1;
2027
+ return true;
2028
+ };
2029
+ if (!expectation.ok && refuses(expectation))
2030
+ return;
1843
2031
  // Try each runtime environment in order and stop at the first that succeeds.
1844
2032
  // Don't gate on fleet.pid: a fleet under systemd/launchd writes its pid in the
1845
2033
  // service's own HOME, which may differ from what this command resolves (sudo,
@@ -1892,8 +2080,8 @@ program
1892
2080
  const unitPath = systemdTarget.user ? getServicePath() : getSystemServicePath();
1893
2081
  const reloadCmd = systemdTarget.user ? "systemctl --user daemon-reload" : "systemctl daemon-reload";
1894
2082
  // #1113 rides the same migration: crash-dump, start-limit and
1895
- // start-timeout directives are filled in alongside KillMode. Only
1896
- // KillMode gates the restart — the others change no stop behaviour.
2083
+ // start-timeout directives are filled in alongside KillMode. KillMode
2084
+ // and the loaded #1071 stop grace gate the restart; the others are informational.
1897
2085
  // The drop-ins systemd applies to this unit, by its own account
1898
2086
  // (DropInPaths), plus any on disk it has not loaded yet. Guessing the
1899
2087
  // directories alone misses type-wide service.d and other load paths.
@@ -1951,6 +2139,23 @@ program
1951
2139
  return;
1952
2140
  }
1953
2141
  }
2142
+ // Migration preserves operator overrides. Admission independently requires
2143
+ // the actual loaded budget for EVERY selected systemd target, even if its
2144
+ // unit file disappeared or no migration/classification was possible.
2145
+ if (hardening.TimeoutStopSec === "custom") {
2146
+ console.log(` ⚠ ${unitPath} sets its own TimeoutStopSec; left as is. A short grace can cut off busy CLI batches (#1071).`);
2147
+ }
2148
+ let loadedStopGrace = "";
2149
+ try {
2150
+ loadedStopGrace = execSync(`systemctl${scopeFlag} show -p TimeoutStopUSec --value ${systemdTarget.unit}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
2151
+ }
2152
+ catch { /* unknown refuses */ }
2153
+ const milliseconds = systemdStopTimeoutMs(loadedStopGrace);
2154
+ if (milliseconds === null || milliseconds < FLEET_STOP_TIMEOUT_MS) {
2155
+ console.error(` ✗ Not restarting: systemd has ${loadedStopGrace || "unknown"} stop grace loaded; this fleet needs ${FLEET_STOP_TIMEOUT_MS / 1000}s. Set TimeoutStopSec to at least ${FLEET_STOP_TIMEOUT_MS / 1000}s in ${unitPath ?? systemdTarget.unit}, run \`${reloadCmd}\`, and retry (#1071).`);
2156
+ process.exitCode = 1;
2157
+ return;
2158
+ }
1954
2159
  if (outcome === "added") {
1955
2160
  console.log(` ✓ ${unitPath}: added KillMode=mixed (the fleet stops its CLIs; systemd no longer signals them all at once)`);
1956
2161
  }
@@ -1958,7 +2163,7 @@ program
1958
2163
  .filter(([key, state]) => key !== "KillMode" && (state === "added" || state === "upgraded"))
1959
2164
  .map(([key]) => key);
1960
2165
  if (updated.length > 0)
1961
- console.log(` ✓ ${unitPath}: updated ${updated.join(", ")} (#1113: small crash dumps, bounded start, start limit)`);
2166
+ console.log(` ✓ ${unitPath}: updated ${updated.join(", ")} (#908/#1071/#1113: graceful stop, small crash dumps, bounded start, start limit)`);
1962
2167
  // Re-judged after the reload, against what systemd now applies: an
1963
2168
  // operator's own mask (anywhere — main file, unit or type drop-in, or a
1964
2169
  // trailing empty assignment that restores the inherited one) is warned
@@ -1989,6 +2194,8 @@ program
1989
2194
  }
1990
2195
  if (filterCustom)
1991
2196
  console.log(` ⚠ ${unitPath} sets its own CoredumpFilter; left as is. CoredumpFilter=0 keeps crash dumps to a few KB (#1113).`);
2197
+ if (expectation.ok && refuses(guard.guardSystemd(captureRun, systemdTarget.user, systemdTarget.unit, expectation.expected, guardDeps)))
2198
+ return;
1992
2199
  // Clears the failed state AND the start-limit counter (#1113:
1993
2200
  // StartLimitBurst=4 in 30min) for whichever unit this restart targets,
1994
2201
  // so an operator's restart is never refused by the limit.
@@ -2015,62 +2222,177 @@ program
2015
2222
  process.exitCode = 1;
2016
2223
  return;
2017
2224
  }
2018
- // 3. launchd (macOS)
2225
+ // 3. launchd (macOS). "Loaded" is launchctl print's own answer (113 = not loaded); nothing here may fall through
2226
+ // to the detached restart once a job is known to be loaded.
2019
2227
  if (plat === "macos") {
2020
2228
  const uid = process.getuid?.() ?? 501;
2021
2229
  const label = "com.agend.fleet";
2022
- try {
2023
- // print succeeds only if the service is loaded in launchd
2024
- execSync(`launchctl print gui/${uid}/${label}`, { stdio: "pipe", timeout: 5000 });
2025
- if (run(`launchctl kickstart -k gui/${uid}/${label}`)) {
2230
+ const target = `gui/${uid}/${label}`;
2231
+ const deps = planDeps();
2232
+ const printed = deps.run("launchctl", ["print", target]);
2233
+ if (printed.status === 0) {
2234
+ const plistPath = getServicePath() ?? join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
2235
+ // Path 2 (#1450 C6): a proven new plist waits on disk with its plan — exactly one bootout + bootstrap.
2236
+ const { readPlan, admitPlan, planPath } = await import("./service-plan.js");
2237
+ const plan = opts.force ? null : readPlan(DATA_DIR, deps);
2238
+ if (plan && expectation.ok) {
2239
+ const pkgDir = dirname(dirname(expectation.expected.entry));
2240
+ const admitted = admitPlan(plan, { target, expected: expectation.expected, pkgDir }, deps);
2241
+ if (!admitted.ok) {
2242
+ console.error(` ✗ Not activating the planned launchd job: ${admitted.reason}. Nothing was stopped.`);
2243
+ process.exitCode = 1;
2244
+ return;
2245
+ }
2246
+ const { activateService } = await import("./service-activation.js");
2247
+ const outcome = activateService({ kind: "launchd", label, plistPath: plan.plistPath, domain: `gui/${uid}` }, { bin: expectation.expected.entry, entry: expectation.expected.entry, node: expectation.expected.node, dir: pkgDir }, {
2248
+ ...deps,
2249
+ readFirstLine: path => deps.readFile(path)?.split("\n", 1)[0] ?? null,
2250
+ isExecutable: path => { try {
2251
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
2252
+ }
2253
+ catch {
2254
+ return false;
2255
+ } },
2256
+ refresh: () => ({ status: 0, signal: null, stdout: "", stderr: "" }),
2257
+ restart: () => { throw new Error("planned launchd activation does not invoke restart"); },
2258
+ log: message => console.log(message),
2259
+ launchdPreimage: plan.preimage.plist,
2260
+ });
2261
+ deps.removeFile(planPath(DATA_DIR));
2262
+ if (outcome.ok) {
2263
+ console.log("Service restarted (launchd loaded the planned job).");
2264
+ return;
2265
+ }
2266
+ console.error(outcome.message);
2267
+ process.exitCode = 1;
2268
+ return;
2269
+ }
2270
+ // Path 1: an unchanged loaded job.
2271
+ if (expectation.ok && refuses(guard.guardLaunchd(captureRun, target, plistPath, deps.readFile, expectation.expected, guardDeps)))
2272
+ return;
2273
+ if (run(`launchctl kickstart -k ${target}`)) {
2026
2274
  console.log("Service restarted.");
2027
2275
  return;
2028
2276
  }
2277
+ console.error(` ✗ launchctl kickstart -k ${target} failed.`);
2278
+ process.exitCode = 1;
2279
+ return;
2280
+ }
2281
+ const { LAUNCHD_NOT_LOADED } = await import("./service-plan.js");
2282
+ if (!LAUNCHD_NOT_LOADED.includes(printed.status ?? -1) || printed.signal !== null) {
2283
+ console.error(` ✗ Could not tell whether ${label} is loaded (launchctl print ${printed.signal ? `killed by ${printed.signal}` : `exited ${printed.status}`}); refusing a detached restart that could duplicate the fleet.`);
2284
+ process.exitCode = 1;
2285
+ return;
2029
2286
  }
2030
- catch { /* not loaded — fall through */ }
2031
2287
  }
2032
2288
  // 4. Detached process tracked by fleet.pid (no service manager)
2033
2289
  if (existsSync(pidPath)) {
2034
- const oldPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
2035
- // Only signal a process confirmed to BE this fleet (#1125 review): a
2036
- // stale fleet.pid may name a recycled pid — an unrelated process. Not
2037
- // confirmed (gone, or something else) is treated as a stale pid file.
2038
- const isOurFleet = () => {
2290
+ const originalPidFile = readFileSync(pidPath, "utf-8");
2291
+ const oldPid = Number(originalPidFile.trim());
2292
+ let claimedStart = null;
2293
+ const absence = () => {
2039
2294
  try {
2040
2295
  process.kill(oldPid, 0);
2041
2296
  }
2042
- catch {
2043
- return false;
2297
+ catch (error) {
2298
+ return error.code === "ESRCH" ? "gone" : "unknown";
2044
2299
  }
2045
- return isFleetStartCommandLine(readProcessCommandLine(oldPid));
2300
+ return detachedProcessExited(oldPid) ? "gone" : null;
2046
2301
  };
2047
- if (oldPid && isOurFleet()) {
2302
+ const inspect = () => {
2048
2303
  try {
2049
- process.kill(oldPid, "SIGTERM");
2304
+ if (existsSync(pidPath) && readFileSync(pidPath, "utf-8") !== originalPidFile)
2305
+ return "unknown";
2050
2306
  }
2051
- catch { /* already gone */ }
2052
- for (let i = 0; i < 20; i++) {
2053
- try {
2054
- process.kill(oldPid, 0);
2055
- }
2056
- catch {
2057
- break;
2058
- }
2059
- spawnSync("sleep", ["0.5"]);
2307
+ catch {
2308
+ return "unknown";
2309
+ }
2310
+ const missing = absence();
2311
+ if (missing)
2312
+ return missing;
2313
+ const start = detachedProcessStart(oldPid);
2314
+ if (start === null)
2315
+ return absence() ?? "unknown";
2316
+ const command = readProcessCommandLine(oldPid);
2317
+ if (!command)
2318
+ return absence() ?? (claimedStart !== null && start === claimedStart ? "waiting" : "unknown");
2319
+ const fleet = isFleetStartCommandLine(command);
2320
+ if (claimedStart !== null && start !== claimedStart)
2321
+ return fleet ? "unknown" : "other";
2322
+ // The claimed process is still alive: rewritten argv is not exit proof.
2323
+ if (claimedStart !== null && !fleet)
2324
+ return "unknown";
2325
+ claimedStart ??= start;
2326
+ return fleet ? "fleet" : "other";
2327
+ };
2328
+ if (!Number.isSafeInteger(oldPid) || oldPid <= 1) {
2329
+ console.error(" ✗ Invalid detached fleet PID; nothing was stopped or started.");
2330
+ process.exitCode = 1;
2331
+ return;
2332
+ }
2333
+ const state = inspect();
2334
+ if (state === "unknown") {
2335
+ console.error(" ✗ Cannot confirm the detached fleet owner; nothing was stopped or started.");
2336
+ process.exitCode = 1;
2337
+ return;
2338
+ }
2339
+ if (state === "fleet") {
2340
+ if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
2341
+ return;
2342
+ console.log(`Stopping detached fleet (up to ${FLEET_STOP_TIMEOUT_MS / 60_000} minutes for graceful shutdown)...`);
2343
+ try {
2344
+ await stopDetachedOwner({
2345
+ inspect,
2346
+ signal: signal => {
2347
+ if (signal === "SIGKILL")
2348
+ console.log(" ⚠ Grace expired; forcing the confirmed fleet owner to exit.");
2349
+ // stdout can block. The helper's earlier proof cannot authorize a
2350
+ // signal after that write; prove the exact owner again now.
2351
+ const owner = inspect();
2352
+ if (owner === "gone" || owner === "other")
2353
+ return;
2354
+ if (owner !== "fleet")
2355
+ throw new Error("Detached fleet ownership changed before signalling; refusing the signal");
2356
+ try {
2357
+ process.kill(oldPid, signal);
2358
+ }
2359
+ catch (error) {
2360
+ if (error.code !== "ESRCH")
2361
+ throw error;
2362
+ }
2363
+ },
2364
+ now: () => performance.now(),
2365
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
2366
+ });
2060
2367
  }
2061
- // Re-confirmed right before the SIGKILL: the pid may have been reused
2062
- // during the wait.
2063
- if (isOurFleet()) {
2064
- try {
2065
- process.kill(oldPid, "SIGKILL");
2066
- }
2067
- catch { /* already gone */ }
2368
+ catch (error) {
2369
+ console.error(` ✗ ${error.message}`);
2370
+ process.exitCode = 1;
2371
+ return;
2372
+ }
2373
+ if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
2374
+ return;
2375
+ const start = selfCommand(["fleet", "start"]);
2376
+ // The runtime guard and command preparation can read files. Recheck
2377
+ // publication AFTER both, immediately before cleanup/spawn. Unreadable
2378
+ // publication cannot authorize deleting a newly published owner's file.
2379
+ let publicationCurrent = false;
2380
+ try {
2381
+ publicationCurrent = readFileSync(pidPath, "utf-8") === originalPidFile;
2382
+ }
2383
+ catch (error) {
2384
+ publicationCurrent = error?.code === "ENOENT";
2385
+ }
2386
+ if (!publicationCurrent) {
2387
+ console.error(" ✗ Fleet owner changed or its publication is unreadable; refusing a duplicate restart.");
2388
+ process.exitCode = 1;
2389
+ return;
2068
2390
  }
2069
2391
  try {
2070
2392
  unlinkSync(pidPath);
2071
2393
  }
2072
- catch { /* best effort */ }
2073
- const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
2394
+ catch { /* old owner already removed it */ }
2395
+ const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
2074
2396
  child.unref();
2075
2397
  console.log("Fleet restarting in background.");
2076
2398
  return;