@mono-agent/agent-app 0.11.2 → 0.11.4

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 (374) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +222 -26
  3. package/dist/adapter-send-tools-main.js +7 -1
  4. package/dist/adapter-send-tools-main.js.map +1 -1
  5. package/dist/adapter-send-tools.d.ts +19 -1
  6. package/dist/adapter-send-tools.d.ts.map +1 -1
  7. package/dist/adapter-send-tools.js +127 -13
  8. package/dist/adapter-send-tools.js.map +1 -1
  9. package/dist/app-config.d.ts.map +1 -1
  10. package/dist/app-config.js +15 -2
  11. package/dist/app-config.js.map +1 -1
  12. package/dist/app-controller-channels.d.ts +8 -0
  13. package/dist/app-controller-channels.d.ts.map +1 -0
  14. package/dist/app-controller-channels.js +207 -0
  15. package/dist/app-controller-channels.js.map +1 -0
  16. package/dist/app-controller-continuation.d.ts +26 -0
  17. package/dist/app-controller-continuation.d.ts.map +1 -0
  18. package/dist/app-controller-continuation.js +198 -0
  19. package/dist/app-controller-continuation.js.map +1 -0
  20. package/dist/app-controller-lifecycle.d.ts +6 -0
  21. package/dist/app-controller-lifecycle.d.ts.map +1 -0
  22. package/dist/app-controller-lifecycle.js +81 -0
  23. package/dist/app-controller-lifecycle.js.map +1 -0
  24. package/dist/app-controller-maintenance.d.ts +13 -0
  25. package/dist/app-controller-maintenance.d.ts.map +1 -0
  26. package/dist/app-controller-maintenance.js +180 -0
  27. package/dist/app-controller-maintenance.js.map +1 -0
  28. package/dist/app-controller-memory-health.d.ts +12 -0
  29. package/dist/app-controller-memory-health.d.ts.map +1 -0
  30. package/dist/app-controller-memory-health.js +160 -0
  31. package/dist/app-controller-memory-health.js.map +1 -0
  32. package/dist/app-controller-memory.d.ts +9 -0
  33. package/dist/app-controller-memory.d.ts.map +1 -0
  34. package/dist/app-controller-memory.js +83 -0
  35. package/dist/app-controller-memory.js.map +1 -0
  36. package/dist/app-controller-responder.d.ts +20 -0
  37. package/dist/app-controller-responder.d.ts.map +1 -0
  38. package/dist/app-controller-responder.js +246 -0
  39. package/dist/app-controller-responder.js.map +1 -0
  40. package/dist/app-controller-traceability.d.ts +25 -0
  41. package/dist/app-controller-traceability.d.ts.map +1 -0
  42. package/dist/app-controller-traceability.js +418 -0
  43. package/dist/app-controller-traceability.js.map +1 -0
  44. package/dist/app-controller-utils.d.ts +38 -0
  45. package/dist/app-controller-utils.d.ts.map +1 -0
  46. package/dist/app-controller-utils.js +185 -0
  47. package/dist/app-controller-utils.js.map +1 -0
  48. package/dist/app-controller.d.ts +400 -0
  49. package/dist/app-controller.d.ts.map +1 -0
  50. package/dist/app-controller.js +336 -0
  51. package/dist/app-controller.js.map +1 -0
  52. package/dist/app.d.ts +7 -115
  53. package/dist/app.d.ts.map +1 -1
  54. package/dist/app.js +6 -1912
  55. package/dist/app.js.map +1 -1
  56. package/dist/artifact-retention.d.ts +4 -0
  57. package/dist/artifact-retention.d.ts.map +1 -1
  58. package/dist/artifact-retention.js +38 -0
  59. package/dist/artifact-retention.js.map +1 -1
  60. package/dist/backfill.d.ts +3 -1
  61. package/dist/backfill.d.ts.map +1 -1
  62. package/dist/backfill.js +5 -1
  63. package/dist/backfill.js.map +1 -1
  64. package/dist/background-runtime.d.ts +27 -0
  65. package/dist/background-runtime.d.ts.map +1 -1
  66. package/dist/background-runtime.js +383 -205
  67. package/dist/background-runtime.js.map +1 -1
  68. package/dist/background-worker-lease.d.ts.map +1 -1
  69. package/dist/background-worker-lease.js +41 -241
  70. package/dist/background-worker-lease.js.map +1 -1
  71. package/dist/background.d.ts +31 -9
  72. package/dist/background.d.ts.map +1 -1
  73. package/dist/background.js +543 -272
  74. package/dist/background.js.map +1 -1
  75. package/dist/channel-drivers/cron.d.ts +11 -0
  76. package/dist/channel-drivers/cron.d.ts.map +1 -0
  77. package/dist/channel-drivers/cron.js +168 -0
  78. package/dist/channel-drivers/cron.js.map +1 -0
  79. package/dist/channel-drivers/live.d.ts +11 -0
  80. package/dist/channel-drivers/live.d.ts.map +1 -0
  81. package/dist/channel-drivers/live.js +47 -0
  82. package/dist/channel-drivers/live.js.map +1 -0
  83. package/dist/channel-drivers/native-notify.d.ts +27 -0
  84. package/dist/channel-drivers/native-notify.d.ts.map +1 -0
  85. package/dist/channel-drivers/native-notify.js +94 -0
  86. package/dist/channel-drivers/native-notify.js.map +1 -0
  87. package/dist/channel-drivers/openai-api.d.ts +7 -0
  88. package/dist/channel-drivers/openai-api.d.ts.map +1 -0
  89. package/dist/channel-drivers/openai-api.js +63 -0
  90. package/dist/channel-drivers/openai-api.js.map +1 -0
  91. package/dist/channel-drivers/shared.d.ts +4 -0
  92. package/dist/channel-drivers/shared.d.ts.map +1 -0
  93. package/dist/channel-drivers/shared.js +5 -0
  94. package/dist/channel-drivers/shared.js.map +1 -0
  95. package/dist/channel-drivers/slack.d.ts +14 -0
  96. package/dist/channel-drivers/slack.d.ts.map +1 -0
  97. package/dist/channel-drivers/slack.js +227 -0
  98. package/dist/channel-drivers/slack.js.map +1 -0
  99. package/dist/channel-drivers/telegram.d.ts +11 -0
  100. package/dist/channel-drivers/telegram.d.ts.map +1 -0
  101. package/dist/channel-drivers/telegram.js +244 -0
  102. package/dist/channel-drivers/telegram.js.map +1 -0
  103. package/dist/channel-drivers/tui.d.ts +15 -0
  104. package/dist/channel-drivers/tui.d.ts.map +1 -0
  105. package/dist/channel-drivers/tui.js +108 -0
  106. package/dist/channel-drivers/tui.js.map +1 -0
  107. package/dist/channel-drivers/webhook.d.ts +7 -0
  108. package/dist/channel-drivers/webhook.d.ts.map +1 -0
  109. package/dist/channel-drivers/webhook.js +115 -0
  110. package/dist/channel-drivers/webhook.js.map +1 -0
  111. package/dist/channels.d.ts +21 -81
  112. package/dist/channels.d.ts.map +1 -1
  113. package/dist/channels.js +11 -1174
  114. package/dist/channels.js.map +1 -1
  115. package/dist/cli-args.d.ts +121 -0
  116. package/dist/cli-args.d.ts.map +1 -0
  117. package/dist/cli-args.js +532 -0
  118. package/dist/cli-args.js.map +1 -0
  119. package/dist/cli-background-command.d.ts +59 -0
  120. package/dist/cli-background-command.d.ts.map +1 -0
  121. package/dist/cli-background-command.js +475 -0
  122. package/dist/cli-background-command.js.map +1 -0
  123. package/dist/cli-cancellation.d.ts +6 -0
  124. package/dist/cli-cancellation.d.ts.map +1 -0
  125. package/dist/cli-cancellation.js +23 -0
  126. package/dist/cli-cancellation.js.map +1 -0
  127. package/dist/cli-commands.d.ts +12 -0
  128. package/dist/cli-commands.d.ts.map +1 -0
  129. package/dist/cli-commands.js +306 -0
  130. package/dist/cli-commands.js.map +1 -0
  131. package/dist/cli-help.d.ts +11 -0
  132. package/dist/cli-help.d.ts.map +1 -0
  133. package/dist/cli-help.js +262 -0
  134. package/dist/cli-help.js.map +1 -0
  135. package/dist/cli-init-command.d.ts +60 -0
  136. package/dist/cli-init-command.d.ts.map +1 -0
  137. package/dist/cli-init-command.js +1910 -0
  138. package/dist/cli-init-command.js.map +1 -0
  139. package/dist/cli-install-skill-command.d.ts +3 -0
  140. package/dist/cli-install-skill-command.d.ts.map +1 -0
  141. package/dist/cli-install-skill-command.js +51 -0
  142. package/dist/cli-install-skill-command.js.map +1 -0
  143. package/dist/cli-sandbox-command.d.ts +10 -0
  144. package/dist/cli-sandbox-command.d.ts.map +1 -0
  145. package/dist/cli-sandbox-command.js +100 -0
  146. package/dist/cli-sandbox-command.js.map +1 -0
  147. package/dist/cli-validate-config-command.d.ts +36 -0
  148. package/dist/cli-validate-config-command.d.ts.map +1 -0
  149. package/dist/cli-validate-config-command.js +317 -0
  150. package/dist/cli-validate-config-command.js.map +1 -0
  151. package/dist/cli.d.ts +6 -248
  152. package/dist/cli.d.ts.map +1 -1
  153. package/dist/cli.js +6 -3792
  154. package/dist/cli.js.map +1 -1
  155. package/dist/config-reference.d.ts +2 -1
  156. package/dist/config-reference.d.ts.map +1 -1
  157. package/dist/config-reference.js +34 -8
  158. package/dist/config-reference.js.map +1 -1
  159. package/dist/configured-agent.d.ts +23 -0
  160. package/dist/configured-agent.d.ts.map +1 -1
  161. package/dist/configured-agent.js +92 -13
  162. package/dist/configured-agent.js.map +1 -1
  163. package/dist/continuation-dispatch-worker.d.ts +21 -0
  164. package/dist/continuation-dispatch-worker.d.ts.map +1 -0
  165. package/dist/continuation-dispatch-worker.js +425 -0
  166. package/dist/continuation-dispatch-worker.js.map +1 -0
  167. package/dist/continuation-http-routes.d.ts +15 -0
  168. package/dist/continuation-http-routes.d.ts.map +1 -0
  169. package/dist/continuation-http-routes.js +265 -0
  170. package/dist/continuation-http-routes.js.map +1 -0
  171. package/dist/continuation-operator-api.d.ts +18 -0
  172. package/dist/continuation-operator-api.d.ts.map +1 -0
  173. package/dist/continuation-operator-api.js +164 -0
  174. package/dist/continuation-operator-api.js.map +1 -0
  175. package/dist/continuation-origin-store.d.ts +19 -0
  176. package/dist/continuation-origin-store.d.ts.map +1 -0
  177. package/dist/continuation-origin-store.js +273 -0
  178. package/dist/continuation-origin-store.js.map +1 -0
  179. package/dist/continuation-service-errors.d.ts +18 -0
  180. package/dist/continuation-service-errors.d.ts.map +1 -0
  181. package/dist/continuation-service-errors.js +35 -0
  182. package/dist/continuation-service-errors.js.map +1 -0
  183. package/dist/continuation-service-helpers.d.ts +39 -0
  184. package/dist/continuation-service-helpers.d.ts.map +1 -0
  185. package/dist/continuation-service-helpers.js +308 -0
  186. package/dist/continuation-service-helpers.js.map +1 -0
  187. package/dist/continuation-service-origin.d.ts +55 -0
  188. package/dist/continuation-service-origin.d.ts.map +1 -0
  189. package/dist/continuation-service-origin.js +325 -0
  190. package/dist/continuation-service-origin.js.map +1 -0
  191. package/dist/continuation-service-types.d.ts +110 -0
  192. package/dist/continuation-service-types.d.ts.map +1 -0
  193. package/dist/continuation-service-types.js +11 -0
  194. package/dist/continuation-service-types.js.map +1 -0
  195. package/dist/continuation-service.d.ts +4 -97
  196. package/dist/continuation-service.d.ts.map +1 -1
  197. package/dist/continuation-service.js +9 -1525
  198. package/dist/continuation-service.js.map +1 -1
  199. package/dist/continuation-store-fs.d.ts +15 -0
  200. package/dist/continuation-store-fs.d.ts.map +1 -0
  201. package/dist/continuation-store-fs.js +247 -0
  202. package/dist/continuation-store-fs.js.map +1 -0
  203. package/dist/continuation-store-policy.d.ts +21 -0
  204. package/dist/continuation-store-policy.d.ts.map +1 -0
  205. package/dist/continuation-store-policy.js +278 -0
  206. package/dist/continuation-store-policy.js.map +1 -0
  207. package/dist/continuation-store-records.d.ts +9 -0
  208. package/dist/continuation-store-records.d.ts.map +1 -0
  209. package/dist/continuation-store-records.js +221 -0
  210. package/dist/continuation-store-records.js.map +1 -0
  211. package/dist/continuation-store-types.d.ts +173 -0
  212. package/dist/continuation-store-types.d.ts.map +1 -0
  213. package/dist/continuation-store-types.js +26 -0
  214. package/dist/continuation-store-types.js.map +1 -0
  215. package/dist/continuation-store.d.ts +4 -138
  216. package/dist/continuation-store.d.ts.map +1 -1
  217. package/dist/continuation-store.js +145 -1049
  218. package/dist/continuation-store.js.map +1 -1
  219. package/dist/doctor.d.ts +10 -5
  220. package/dist/doctor.d.ts.map +1 -1
  221. package/dist/doctor.js +629 -58
  222. package/dist/doctor.js.map +1 -1
  223. package/dist/init.d.ts +2 -0
  224. package/dist/init.d.ts.map +1 -1
  225. package/dist/init.js +92 -239
  226. package/dist/init.js.map +1 -1
  227. package/dist/interaction-bridge.d.ts +19 -0
  228. package/dist/interaction-bridge.d.ts.map +1 -1
  229. package/dist/interaction-bridge.js +130 -0
  230. package/dist/interaction-bridge.js.map +1 -1
  231. package/dist/launchd-logs.d.ts +91 -0
  232. package/dist/launchd-logs.d.ts.map +1 -0
  233. package/dist/launchd-logs.js +1499 -0
  234. package/dist/launchd-logs.js.map +1 -0
  235. package/dist/launchd.d.ts +27 -0
  236. package/dist/launchd.d.ts.map +1 -1
  237. package/dist/launchd.js +89 -10
  238. package/dist/launchd.js.map +1 -1
  239. package/dist/memory-command.d.ts +2 -0
  240. package/dist/memory-command.d.ts.map +1 -1
  241. package/dist/memory-command.js +161 -48
  242. package/dist/memory-command.js.map +1 -1
  243. package/dist/memory-recall-settings.d.ts +64 -0
  244. package/dist/memory-recall-settings.d.ts.map +1 -0
  245. package/dist/memory-recall-settings.js +51 -0
  246. package/dist/memory-recall-settings.js.map +1 -0
  247. package/dist/memory-recall.d.ts +7 -95
  248. package/dist/memory-recall.d.ts.map +1 -1
  249. package/dist/memory-recall.js +8 -143
  250. package/dist/memory-recall.js.map +1 -1
  251. package/dist/memory-rituals.d.ts +3 -3
  252. package/dist/memory-rituals.d.ts.map +1 -1
  253. package/dist/memory-rituals.js +16 -111
  254. package/dist/memory-rituals.js.map +1 -1
  255. package/dist/modules/catalog.js +2 -2
  256. package/dist/modules/catalog.js.map +1 -1
  257. package/dist/modules/known-tools.d.ts +8 -6
  258. package/dist/modules/known-tools.d.ts.map +1 -1
  259. package/dist/modules/known-tools.js +8 -6
  260. package/dist/modules/known-tools.js.map +1 -1
  261. package/dist/notify-destinations.d.ts +5 -0
  262. package/dist/notify-destinations.d.ts.map +1 -1
  263. package/dist/notify-destinations.js +7 -1
  264. package/dist/notify-destinations.js.map +1 -1
  265. package/dist/owner-private-lock.d.ts +95 -0
  266. package/dist/owner-private-lock.d.ts.map +1 -0
  267. package/dist/owner-private-lock.js +566 -0
  268. package/dist/owner-private-lock.js.map +1 -0
  269. package/dist/pi-oauth-login.d.ts +2 -0
  270. package/dist/pi-oauth-login.d.ts.map +1 -1
  271. package/dist/pi-oauth-login.js +39 -5
  272. package/dist/pi-oauth-login.js.map +1 -1
  273. package/dist/posted-message-index.d.ts +35 -16
  274. package/dist/posted-message-index.d.ts.map +1 -1
  275. package/dist/posted-message-index.js +1068 -52
  276. package/dist/posted-message-index.js.map +1 -1
  277. package/dist/posted-reply-history.d.ts +17 -0
  278. package/dist/posted-reply-history.d.ts.map +1 -0
  279. package/dist/posted-reply-history.js +169 -0
  280. package/dist/posted-reply-history.js.map +1 -0
  281. package/dist/proactive-notify.d.ts.map +1 -1
  282. package/dist/proactive-notify.js +6 -4
  283. package/dist/proactive-notify.js.map +1 -1
  284. package/dist/project-skills.d.ts +4 -0
  285. package/dist/project-skills.d.ts.map +1 -1
  286. package/dist/project-skills.js +99 -130
  287. package/dist/project-skills.js.map +1 -1
  288. package/dist/provider-setup.d.ts +0 -1
  289. package/dist/provider-setup.d.ts.map +1 -1
  290. package/dist/provider-setup.js +0 -3
  291. package/dist/provider-setup.js.map +1 -1
  292. package/dist/readiness-probe-worker.d.ts +61 -1
  293. package/dist/readiness-probe-worker.d.ts.map +1 -1
  294. package/dist/readiness-probe-worker.js +54 -58
  295. package/dist/readiness-probe-worker.js.map +1 -1
  296. package/dist/readiness-probe.d.ts +6 -9
  297. package/dist/readiness-probe.d.ts.map +1 -1
  298. package/dist/readiness-probe.js +30 -56
  299. package/dist/readiness-probe.js.map +1 -1
  300. package/dist/redact-secrets.d.ts +15 -0
  301. package/dist/redact-secrets.d.ts.map +1 -0
  302. package/dist/redact-secrets.js +65 -0
  303. package/dist/redact-secrets.js.map +1 -0
  304. package/dist/run-history.js +34 -24
  305. package/dist/run-history.js.map +1 -1
  306. package/dist/runtime-provenance.d.ts +8 -0
  307. package/dist/runtime-provenance.d.ts.map +1 -0
  308. package/dist/runtime-provenance.js +225 -0
  309. package/dist/runtime-provenance.js.map +1 -0
  310. package/dist/sandbox-manager.d.ts +4 -1
  311. package/dist/sandbox-manager.d.ts.map +1 -1
  312. package/dist/sandbox-manager.js +114 -273
  313. package/dist/sandbox-manager.js.map +1 -1
  314. package/dist/secure-file-replace.d.ts +49 -0
  315. package/dist/secure-file-replace.d.ts.map +1 -0
  316. package/dist/secure-file-replace.js +270 -0
  317. package/dist/secure-file-replace.js.map +1 -0
  318. package/dist/seen-conversations.d.ts +32 -5
  319. package/dist/seen-conversations.d.ts.map +1 -1
  320. package/dist/seen-conversations.js +145 -15
  321. package/dist/seen-conversations.js.map +1 -1
  322. package/dist/sessions.d.ts +20 -0
  323. package/dist/sessions.d.ts.map +1 -1
  324. package/dist/sessions.js +41 -6
  325. package/dist/sessions.js.map +1 -1
  326. package/dist/wizard/index.d.ts +2 -2
  327. package/dist/wizard/index.d.ts.map +1 -1
  328. package/dist/wizard/index.js +1 -1
  329. package/dist/wizard/index.js.map +1 -1
  330. package/dist/wizard/presets.d.ts +2 -1
  331. package/dist/wizard/presets.d.ts.map +1 -1
  332. package/dist/wizard/presets.js +2 -1
  333. package/dist/wizard/presets.js.map +1 -1
  334. package/dist/wizard/run.d.ts +0 -26
  335. package/dist/wizard/run.d.ts.map +1 -1
  336. package/dist/wizard/run.js +0 -53
  337. package/dist/wizard/run.js.map +1 -1
  338. package/package.json +22 -17
  339. package/schema/mono-agent.config.schema.json +45 -10
  340. package/skills/mono-agent-composer/SKILL.md +3 -3
  341. package/skills/mono-agent-composer/references/config-blueprint.md +123 -29
  342. package/skills/mono-agent-composer/references/discovery-questions.md +10 -8
  343. package/skills/mono-agent-composer/references/feature-coverage.md +90 -76
  344. package/skills/mono-agent-composer/references/package-map.md +6 -4
  345. package/skills/mono-agent-composer/references/playbooks.md +48 -27
  346. package/skills/mono-agent-composer/references/validation.md +16 -3
  347. package/dist/notify-runtime.d.ts +0 -26
  348. package/dist/notify-runtime.d.ts.map +0 -1
  349. package/dist/notify-runtime.js +0 -27
  350. package/dist/notify-runtime.js.map +0 -1
  351. package/dist/notify-tool.d.ts +0 -51
  352. package/dist/notify-tool.d.ts.map +0 -1
  353. package/dist/notify-tool.js +0 -182
  354. package/dist/notify-tool.js.map +0 -1
  355. package/dist/recipes/base.d.ts +0 -15
  356. package/dist/recipes/base.d.ts.map +0 -1
  357. package/dist/recipes/base.js +0 -51
  358. package/dist/recipes/base.js.map +0 -1
  359. package/dist/recipes/catalog.d.ts +0 -4
  360. package/dist/recipes/catalog.d.ts.map +0 -1
  361. package/dist/recipes/catalog.js +0 -525
  362. package/dist/recipes/catalog.js.map +0 -1
  363. package/dist/recipes/index.d.ts +0 -11
  364. package/dist/recipes/index.d.ts.map +0 -1
  365. package/dist/recipes/index.js +0 -14
  366. package/dist/recipes/index.js.map +0 -1
  367. package/dist/recipes/types.d.ts +0 -70
  368. package/dist/recipes/types.d.ts.map +0 -1
  369. package/dist/recipes/types.js +0 -15
  370. package/dist/recipes/types.js.map +0 -1
  371. package/dist/setup.d.ts +0 -29
  372. package/dist/setup.d.ts.map +0 -1
  373. package/dist/setup.js +0 -97
  374. package/dist/setup.js.map +0 -1
@@ -1,21 +1,24 @@
1
1
  import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { constants as fsConstants } from "node:fs";
4
- import { lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
+ import { lstat, mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
  import process from "node:process";
7
7
  import { listRecordedRuns, listTraceSources } from "@mono-agent/observability";
8
8
  import { describeSensitiveDataExportWarning, phoenixAppBaseUrl, resolveAppTraceRegistryDir, resolveAppTraceStaleAfterMs, } from "./app-config.js";
9
9
  import { formatChannelFactValue } from "./channel-fact-format.js";
10
- import { bootout, bootstrap, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, isLoaded, launchdServiceInfo, launchdPathsFor, makeLaunchctlRunner, } from "./launchd.js";
10
+ import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
11
11
  import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
12
+ import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, markLaunchdLogMaintenanceStopping, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
12
13
  import { ensureManagedBackgroundRuntime, MANAGED_BACKGROUND_WORKER_ENV, } from "./background-runtime.js";
13
14
  import { backgroundSnapshotFromMetadata, captureBackgroundSnapshot, encodeBackgroundSnapshot, sameBackgroundSnapshot, } from "./background-snapshot.js";
14
15
  import { resolveConfiguredManagedRuntimePackages } from "./managed-runtime-packages.js";
15
- import { currentProcessIncarnation, isSameProcessIncarnation as matchesProcessIncarnation, processIncarnationFromJson, } from "./process-incarnation.js";
16
+ import { processIncarnationFromJson, } from "./process-incarnation.js";
17
+ import { acquireOwnerPrivateLock, validateOwnerPrivateLockInputs } from "./owner-private-lock.js";
16
18
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
17
19
  import * as ui from "./ui.js";
18
20
  export { acquireBackgroundWorkerLease, backgroundWorkerLeasePath, } from "./background-worker-lease.js";
21
+ export { LAUNCHD_LOG_MAX_BYTES, LAUNCHD_LOG_ROTATION_COUNT, } from "./launchd-logs.js";
19
22
  /** Exact non-secret environment materialised into a managed LaunchAgent. */
20
23
  export function managedBackgroundEnvironment(env) {
21
24
  return {
@@ -26,6 +29,16 @@ export function managedBackgroundEnvironment(env) {
26
29
  [MANAGED_BACKGROUND_WORKER_ENV]: "1",
27
30
  };
28
31
  }
32
+ /** Exact non-secret environment for the scheduled one-shot log maintainer. */
33
+ export function managedLaunchdLogMaintenanceEnvironment(env) {
34
+ return {
35
+ ...selectBackgroundOperationalEnvironment(env),
36
+ // This unattended helper needs only pinned system tools. Do not persist a
37
+ // caller-controlled PATH that could shadow launchctl on a later run.
38
+ PATH: "/usr/bin:/bin",
39
+ [MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV]: "1",
40
+ };
41
+ }
29
42
  /**
30
43
  * Resolve everything the control commands need from CLI args. The registry dir
31
44
  * and config path are resolved exactly as the worker will resolve them, so the
@@ -101,8 +114,21 @@ export function defaultBackgroundDeps() {
101
114
  writeFile: writeOwnerPrivateLaunchdFile,
102
115
  mkdir: ensureOwnerPrivateLaunchdDirectory,
103
116
  rm: (path) => rm(path, { force: true }),
104
- stat: inspectOwnerPrivateLaunchdLog,
105
- rename,
117
+ inspectLaunchdLogs: async (paths) => await inspectLaunchdLogs(paths),
118
+ rotateStoppedLaunchdLogs: async (paths) => {
119
+ await rotateStoppedLaunchdLogs(paths);
120
+ },
121
+ readLaunchdLogMaintenanceIntent: async (paths) => await readLaunchdLogMaintenanceIntent(paths),
122
+ beginLaunchdLogMaintenanceIntent: async (paths, intent) => {
123
+ await beginLaunchdLogMaintenanceIntent(paths, intent);
124
+ },
125
+ markLaunchdLogMaintenanceStopped: async (paths, expected) => await markLaunchdLogMaintenanceStopped(paths, expected),
126
+ markLaunchdLogMaintenanceRestoring: async (paths, expected) => await markLaunchdLogMaintenanceRestoring(paths, expected),
127
+ markLaunchdLogMaintenanceStopping: async (paths, expected) => await markLaunchdLogMaintenanceStopping(paths, expected),
128
+ clearLaunchdLogMaintenanceIntent: async (paths, expected) => {
129
+ await clearLaunchdLogMaintenanceIntent(paths, expected);
130
+ },
131
+ verifyLaunchdPlist: inspectOwnerPrivateLaunchdPlist,
106
132
  isAlive: (pid) => {
107
133
  try {
108
134
  process.kill(pid, 0);
@@ -131,8 +157,6 @@ export function defaultBackgroundDeps() {
131
157
  };
132
158
  }
133
159
  const DEFAULT_POLL = { timeoutMs: 18_000, intervalMs: 400 };
134
- export const LAUNCHD_LOG_MAX_BYTES = 5 * 1024 * 1024;
135
- export const LAUNCHD_LOG_ROTATION_COUNT = 3;
136
160
  export async function startBackground(target, deps, poll = DEFAULT_POLL) {
137
161
  return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
138
162
  }
@@ -165,6 +189,213 @@ export async function forceRestartBackground(target, deps, whileStopped, poll =
165
189
  });
166
190
  }
167
191
  }
192
+ /**
193
+ * Private one-shot invoked only by the scheduled maintenance LaunchAgent. It
194
+ * never creates or rewrites a service definition. It does not resurrect an
195
+ * intentionally stopped service; the sole exception is recovery authorized by
196
+ * its own durable lifecycle intent after a prior maintainer died post-bootout.
197
+ */
198
+ export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_POLL) {
199
+ const release = await deps.acquireLifecycleLock(target);
200
+ if (release === undefined) {
201
+ // Controller start/restart/stop owns the same lock. Skipping this periodic
202
+ // pass is expected and avoids making launchd record a false maintenance
203
+ // failure; the next interval retries from a fresh inventory.
204
+ return 0;
205
+ }
206
+ try {
207
+ const uid = deps.getuid();
208
+ const service = await launchdServiceInfo(deps.runner, target.label, uid);
209
+ let inspection;
210
+ try {
211
+ inspection = await deps.inspectLaunchdLogs(target.paths);
212
+ }
213
+ catch (error) {
214
+ reportMaintenanceFailure(target, deps, "inspect launchd logs", error);
215
+ return 1;
216
+ }
217
+ let maintenanceIntent;
218
+ try {
219
+ maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
220
+ }
221
+ catch (error) {
222
+ reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", error);
223
+ return 1;
224
+ }
225
+ if (inspection.pendingMaintenance && maintenanceIntent === undefined) {
226
+ reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", new Error("The maintenance marker disappeared after inventory."));
227
+ return 1;
228
+ }
229
+ if (!service.loaded && maintenanceIntent === undefined)
230
+ return 0;
231
+ let originalPlistIdentity;
232
+ try {
233
+ originalPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
234
+ }
235
+ catch (error) {
236
+ reportMaintenanceFailure(target, deps, "verify the existing main LaunchAgent", error);
237
+ return 1;
238
+ }
239
+ if (maintenanceIntent !== undefined
240
+ && (maintenanceIntent.label !== target.label
241
+ || maintenanceIntent.plistFingerprint !== originalPlistIdentity)) {
242
+ reportMaintenanceFailure(target, deps, "authenticate durable launchd-log maintenance intent", new Error("The pending intent does not match the exact main LaunchAgent definition."));
243
+ return 1;
244
+ }
245
+ if (maintenanceIntent?.phase === "stopping" && !service.loaded) {
246
+ reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("The prior maintainer did not durably prove every old writer PID dead; refusing rotation."));
247
+ return 1;
248
+ }
249
+ if (maintenanceIntent?.phase === "restoring") {
250
+ const replacement = await launchdServiceInfo(deps.runner, target.label, uid);
251
+ if (!replacement.loaded || replacement.pid === undefined || !deps.isAlive(replacement.pid)) {
252
+ reportMaintenanceFailure(target, deps, "recover interrupted launchd-log restoration", new Error("The replacement writer identity was lost before live-worker proof; refusing stale rotation authority."));
253
+ return 1;
254
+ }
255
+ try {
256
+ await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
257
+ }
258
+ catch (error) {
259
+ reportMaintenanceFailure(target, deps, "clear recovered launchd-log restoration intent", error);
260
+ return 1;
261
+ }
262
+ return 0;
263
+ }
264
+ if (maintenanceIntent?.phase === "stopped" && service.loaded) {
265
+ reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("launchd reports a writer loaded after durable stopped-writer proof; refusing rotation."));
266
+ return 1;
267
+ }
268
+ if (!inspection.canMaintain) {
269
+ deps.stderr(ui.errorLine(`Scheduled log maintenance refused unsafe paths for ${target.label}.`));
270
+ for (const issue of inspection.issues)
271
+ deps.stderr(ui.style.dim(issue) + "\n");
272
+ return 1;
273
+ }
274
+ if (!inspection.needsMaintenance && maintenanceIntent === undefined)
275
+ return 0;
276
+ if (maintenanceIntent === undefined) {
277
+ maintenanceIntent = {
278
+ version: 1,
279
+ phase: "stopping",
280
+ label: target.label,
281
+ plistFingerprint: originalPlistIdentity,
282
+ };
283
+ try {
284
+ await deps.beginLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
285
+ }
286
+ catch (error) {
287
+ reportMaintenanceFailure(target, deps, "publish durable launchd-log maintenance intent", error);
288
+ return 1;
289
+ }
290
+ }
291
+ if (maintenanceIntent.phase === "stopping") {
292
+ const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid]), deps, uid, poll);
293
+ if (!stopped.ok) {
294
+ reportMaintenanceFailure(target, deps, "prove the launchd log writer stopped", stopped.failure);
295
+ return 1;
296
+ }
297
+ try {
298
+ maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
299
+ }
300
+ catch (error) {
301
+ reportMaintenanceFailure(target, deps, "record durable stopped-writer proof", error);
302
+ return 1;
303
+ }
304
+ }
305
+ else {
306
+ const current = await launchdServiceInfo(deps.runner, target.label, uid);
307
+ if (current.loaded || (current.pid !== undefined && deps.isAlive(current.pid))) {
308
+ reportMaintenanceFailure(target, deps, "recheck durable stopped-writer proof", new Error("launchd exposed a live writer before recovered rotation."));
309
+ return 1;
310
+ }
311
+ }
312
+ try {
313
+ await deps.rotateStoppedLaunchdLogs(target.paths);
314
+ // Close the pathname-replacement race before handing the definition to
315
+ // launchctl. A changed/symlinked plist leaves the worker stopped.
316
+ const currentPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
317
+ if (currentPlistIdentity !== originalPlistIdentity) {
318
+ throw new Error("The main LaunchAgent plist changed during stopped-writer maintenance.");
319
+ }
320
+ }
321
+ catch (error) {
322
+ reportMaintenanceFailure(target, deps, "commit bounded stopped-writer logs", error);
323
+ return 1;
324
+ }
325
+ try {
326
+ maintenanceIntent = await deps.markLaunchdLogMaintenanceRestoring(target.paths, maintenanceIntent);
327
+ }
328
+ catch (error) {
329
+ reportMaintenanceFailure(target, deps, "invalidate stopped-writer proof before restoration", error);
330
+ return 1;
331
+ }
332
+ const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
333
+ const observedRestorePids = new Set();
334
+ const running = await pollUntil(deps, poll, async () => {
335
+ const current = await launchdServiceInfo(deps.runner, target.label, uid);
336
+ if (current.pid !== undefined)
337
+ observedRestorePids.add(current.pid);
338
+ return current.loaded && current.pid !== undefined && deps.isAlive(current.pid);
339
+ });
340
+ if (!running) {
341
+ const current = await launchdServiceInfo(deps.runner, target.label, uid);
342
+ if (current.pid !== undefined)
343
+ observedRestorePids.add(current.pid);
344
+ const cleanedUp = await unloadLaunchdService(target.label, current, [...observedRestorePids], deps, uid, poll);
345
+ reportMaintenanceFailure(target, deps, "restore the exact main LaunchAgent after rotation", lifecycleFailure(booted, "launchd did not expose a live replacement worker"));
346
+ if (!cleanedUp.ok) {
347
+ reportMaintenanceFailure(target, deps, "remove the failed replacement worker", cleanedUp.failure);
348
+ }
349
+ return 1;
350
+ }
351
+ try {
352
+ const restoredPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
353
+ if (restoredPlistIdentity !== originalPlistIdentity) {
354
+ throw new Error("The main LaunchAgent plist changed while launchd restored the worker.");
355
+ }
356
+ }
357
+ catch (error) {
358
+ const restoredService = await launchdServiceInfo(deps.runner, target.label, uid);
359
+ const stoppedAgain = await unloadLaunchdService(target.label, restoredService, uniquePids([restoredService.pid, ...observedRestorePids]), deps, uid, poll);
360
+ reportMaintenanceFailure(target, deps, "prove the restored main LaunchAgent definition", error);
361
+ if (!stoppedAgain.ok) {
362
+ reportMaintenanceFailure(target, deps, "stop the worker after its definition changed", stoppedAgain.failure);
363
+ }
364
+ return 1;
365
+ }
366
+ try {
367
+ await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
368
+ }
369
+ catch (error) {
370
+ reportMaintenanceFailure(target, deps, "clear durable launchd-log maintenance intent", error);
371
+ return 1;
372
+ }
373
+ return 0;
374
+ }
375
+ finally {
376
+ await release().catch((error) => {
377
+ deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
378
+ });
379
+ }
380
+ }
381
+ function reportMaintenanceFailure(target, deps, action, error) {
382
+ deps.stderr(ui.errorLine(`Scheduled log maintenance could not ${action} for ${target.label}: ${maintenanceErrorMessage(error)}`));
383
+ }
384
+ function maintenanceErrorMessage(error) {
385
+ if (error instanceof Error)
386
+ return error.message;
387
+ if (typeof error === "object" && error !== null) {
388
+ const result = error;
389
+ const detail = [result.stderr, result.stdout]
390
+ .find((value) => typeof value === "string" && value.trim().length > 0)
391
+ ?.trim();
392
+ if (detail !== undefined)
393
+ return detail;
394
+ if (typeof result.code === "number")
395
+ return `launchctl exited ${result.code}`;
396
+ }
397
+ return String(error);
398
+ }
168
399
  /**
169
400
  * Ensure the canonical per-config LaunchAgent is running and return the fresh
170
401
  * trace source that proved it reached `startup-complete`. This is the shared
@@ -234,24 +465,50 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
234
465
  reportLifecycleException(launchTarget, deps, "verify the existing background worker ownership", error);
235
466
  return { ok: false, action: "start", reason: "ownership" };
236
467
  }
237
- let sinceMs;
468
+ let sinceMs = deps.now();
238
469
  try {
239
470
  await prepareLaunchdDirectories(launchTarget, deps);
240
- await rotateLaunchdLogs(launchTarget.paths, deps);
241
- await writePlist(launchTarget, deps);
242
- sinceMs = deps.now();
243
471
  }
244
472
  catch (error) {
245
- reportLifecycleException(launchTarget, deps, "prepare the LaunchAgent", error);
473
+ reportLifecycleException(launchTarget, deps, "prepare the LaunchAgent directories", error);
246
474
  return { ok: false, action: "start", reason: "preparation" };
247
475
  }
248
476
  let outcome;
477
+ let prepared = false;
249
478
  try {
250
- outcome = await bootstrapOrRestart(launchTarget, deps, uid, poll);
479
+ let interruptedMaintenance = await deps.readLaunchdLogMaintenanceIntent(launchTarget.paths);
480
+ if (interruptedMaintenance?.phase === "stopping" || interruptedMaintenance?.phase === "restoring") {
481
+ const current = await launchdServiceInfo(deps.runner, launchTarget.label, uid);
482
+ if (!current.loaded) {
483
+ throw new Error("Interrupted launchd-log maintenance lost its writer PID before durable stopped-writer proof; refusing restart.");
484
+ }
485
+ }
486
+ outcome = await bootstrapOrRestart(launchTarget, deps, uid, poll, async () => {
487
+ const stoppedIntent = interruptedMaintenance;
488
+ if (stoppedIntent?.phase === "stopped") {
489
+ interruptedMaintenance = await deps.markLaunchdLogMaintenanceStopping(launchTarget.paths, stoppedIntent);
490
+ }
491
+ }, async (mainStopProven) => {
492
+ const stoppingIntent = interruptedMaintenance;
493
+ if (stoppingIntent?.phase === "stopping" || stoppingIntent?.phase === "restoring") {
494
+ if (!mainStopProven) {
495
+ throw new Error("The controller could not renew stopped-writer proof for interrupted log maintenance.");
496
+ }
497
+ interruptedMaintenance = await deps.markLaunchdLogMaintenanceStopped(launchTarget.paths, stoppingIntent);
498
+ }
499
+ await deps.rotateStoppedLaunchdLogs(launchTarget.paths);
500
+ // A controller may be completing a maintainer that died after bootout.
501
+ // Once journal recovery finishes under the stopped-writer proof, cancel
502
+ // its old restore authority before replacing either plist.
503
+ await deps.clearLaunchdLogMaintenanceIntent(launchTarget.paths, interruptedMaintenance);
504
+ await writePlists(launchTarget, deps);
505
+ sinceMs = deps.now();
506
+ prepared = true;
507
+ });
251
508
  }
252
509
  catch (error) {
253
- reportLifecycleException(launchTarget, deps, "start the LaunchAgent", error);
254
- return { ok: false, action: "start", reason: "launchctl" };
510
+ reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
511
+ return { ok: false, action: "start", reason: prepared ? "launchctl" : "preparation" };
255
512
  }
256
513
  const action = outcome.restarted ? "restart" : "start";
257
514
  if (!outcome.ok) {
@@ -297,11 +554,12 @@ async function prepareLaunchdDirectories(target, deps) {
297
554
  await deps.mkdir(target.paths.logDir);
298
555
  await deps.mkdir(target.paths.launchAgentsDir);
299
556
  }
300
- async function writePlist(target, deps) {
557
+ async function writePlists(target, deps) {
301
558
  if (target.expectedSnapshot === undefined) {
302
559
  throw new Error("A managed LaunchAgent requires an approved background snapshot.");
303
560
  }
304
- const xml = buildPlistXml({
561
+ const maintenance = maintenancePathsForTarget(target);
562
+ const mainXml = buildPlistXml({
305
563
  label: target.label,
306
564
  nodePath: target.nodePath,
307
565
  cliPath: target.cliPath,
@@ -313,38 +571,20 @@ async function writePlist(target, deps) {
313
571
  stderrPath: target.paths.stderrPath,
314
572
  environment: target.environment,
315
573
  });
316
- await deps.writeFile(target.paths.plistPath, xml);
317
- }
318
- async function rotateLaunchdLogs(paths, deps) {
319
- await Promise.all([
320
- rotateLaunchdLog(paths.stdoutPath, deps),
321
- rotateLaunchdLog(paths.stderrPath, deps),
322
- ]);
323
- }
324
- async function rotateLaunchdLog(path, deps) {
325
- let size;
326
- try {
327
- size = (await deps.stat(path)).size;
328
- }
329
- catch (error) {
330
- if (isErrno(error, "ENOENT"))
331
- return;
332
- throw error;
333
- }
334
- if (size <= LAUNCHD_LOG_MAX_BYTES) {
335
- return;
336
- }
337
- await deps.rm(`${path}.${LAUNCHD_LOG_ROTATION_COUNT}`);
338
- for (let index = LAUNCHD_LOG_ROTATION_COUNT; index >= 1; index -= 1) {
339
- const source = index === 1 ? path : `${path}.${index - 1}`;
340
- const destination = `${path}.${index}`;
341
- try {
342
- await deps.rename(source, destination);
343
- }
344
- catch {
345
- // Missing or locked rotation segment: leave it for the next launch cycle.
346
- }
347
- }
574
+ const maintenanceXml = buildLaunchdMaintenancePlistXml({
575
+ label: maintenance.label,
576
+ nodePath: target.nodePath,
577
+ cliPath: target.cliPath,
578
+ configPath: target.configPath,
579
+ // The agent folder may be renamed or removed while its already-running
580
+ // worker still owns log descriptors. Keep the scheduler's cwd in the
581
+ // durable account state tree; its config argument is already absolute.
582
+ cwd: dirname(target.paths.logDir),
583
+ environment: managedLaunchdLogMaintenanceEnvironment(target.environment),
584
+ intervalSeconds: LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS,
585
+ });
586
+ await deps.writeFile(target.paths.plistPath, mainXml);
587
+ await deps.writeFile(maintenance.plistPath, maintenanceXml);
348
588
  }
349
589
  async function ensureOwnerPrivateLaunchdDirectory(path) {
350
590
  const parent = dirname(path);
@@ -431,27 +671,54 @@ async function writeOwnerPrivateLaunchdFile(path, data) {
431
671
  await rm(temporaryPath, { force: true }).catch(() => undefined);
432
672
  }
433
673
  }
434
- async function inspectOwnerPrivateLaunchdLog(path) {
435
- const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
674
+ async function inspectOwnerPrivateLaunchdPlist(path) {
675
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
436
676
  try {
437
677
  const before = await handle.stat();
438
- assertOwnerRegularFile(before, path, "LaunchAgent log");
439
- await handle.chmod(0o600);
440
- const secured = await handle.stat();
441
- if (!sameFilesystemIdentity(before, secured)) {
442
- throw new Error(`LaunchAgent log ${path} changed while it was secured.`);
678
+ assertOwnerRegularFile(before, path, "LaunchAgent plist");
679
+ if ((before.mode & 0o777) !== 0o600) {
680
+ throw new Error(`LaunchAgent plist ${path} must be owner-only.`);
681
+ }
682
+ if (!Number.isSafeInteger(before.size) || before.size < 1 || before.size > 1024 * 1024) {
683
+ throw new Error(`LaunchAgent plist ${path} has an unsafe size.`);
684
+ }
685
+ const contents = await readExactFileHandle(handle, before.size, "LaunchAgent plist");
686
+ const after = await handle.stat();
687
+ assertOwnerRegularFile(after, path, "LaunchAgent plist");
688
+ if ((after.mode & 0o777) !== 0o600
689
+ || !sameFileSnapshot(before, after)
690
+ || contents.length !== before.size) {
691
+ throw new Error(`LaunchAgent plist ${path} changed while it was read.`);
443
692
  }
444
- assertOwnerRegularFile(secured, path, "LaunchAgent log");
445
693
  const current = await lstat(path);
446
- if (!sameFilesystemIdentity(secured, current)) {
447
- throw new Error(`LaunchAgent log ${path} changed while it was secured.`);
694
+ assertOwnerRegularFile(current, path, "LaunchAgent plist");
695
+ if ((current.mode & 0o777) !== 0o600 || !sameFileSnapshot(after, current)) {
696
+ throw new Error(`LaunchAgent plist ${path} changed while it was inspected.`);
448
697
  }
449
- return secured;
698
+ return [
699
+ String(after.dev),
700
+ String(after.ino),
701
+ String(after.size),
702
+ createHash("sha256").update(contents).digest("hex"),
703
+ ].join(":");
450
704
  }
451
705
  finally {
452
706
  await handle.close();
453
707
  }
454
708
  }
709
+ async function readExactFileHandle(handle, size, description) {
710
+ if (!Number.isSafeInteger(size) || size < 0)
711
+ throw new Error(`${description} has an unsafe size.`);
712
+ const contents = Buffer.alloc(size);
713
+ let offset = 0;
714
+ while (offset < size) {
715
+ const result = await handle.read(contents, offset, size - offset, offset);
716
+ if (result.bytesRead === 0)
717
+ throw new Error(`${description} ended while its bounded contents were read.`);
718
+ offset += result.bytesRead;
719
+ }
720
+ return contents;
721
+ }
455
722
  function assertOwnerDirectory(details, path, description) {
456
723
  if (!details.isDirectory() || details.isSymbolicLink()) {
457
724
  throw new Error(`${description} ${path} must be a real directory.`);
@@ -475,52 +742,139 @@ function assertCurrentUserOwns(details, path, description) {
475
742
  function sameFilesystemIdentity(left, right) {
476
743
  return left.dev === right.dev && left.ino === right.ino;
477
744
  }
745
+ function sameFileSnapshot(left, right) {
746
+ return sameFilesystemIdentity(left, right)
747
+ && left.uid === right.uid
748
+ && left.nlink === right.nlink
749
+ && left.mode === right.mode
750
+ && left.size === right.size
751
+ && left.mtimeMs === right.mtimeMs
752
+ && left.ctimeMs === right.ctimeMs;
753
+ }
478
754
  /**
479
755
  * Idempotent: bootstrap when not loaded. A loaded job is always fully removed
480
756
  * before bootstrap so launchd cannot retain stale ProgramArguments or env from
481
757
  * the previous plist.
482
758
  */
483
- async function bootstrapOrRestart(target, deps, uid, poll) {
484
- const service = await launchdServiceInfo(deps.runner, target.label, uid);
485
- if (!service.loaded) {
486
- const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
487
- if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
488
- return { ok: true, restarted: false };
489
- }
490
- return { ok: false, restarted: false, failure: booted };
491
- }
492
- const oldSources = await findInstances(target, deps);
493
- const oldPids = uniquePids([
494
- service.pid,
495
- // A cleanly stopped manifest can outlive its process long enough for the OS
496
- // to reuse that pid for unrelated work. It is historical evidence, not an
497
- // ownership claim, so never wait for its recycled pid during restart.
498
- ...oldSources
499
- .filter((source) => source.health !== "stopped")
500
- .map((source) => source.pid),
759
+ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
760
+ const maintenance = maintenancePathsForTarget(target);
761
+ const [service, maintenanceService] = await Promise.all([
762
+ launchdServiceInfo(deps.runner, target.label, uid),
763
+ launchdServiceInfo(deps.runner, maintenance.label, uid),
501
764
  ]);
502
- const removed = await bootout(deps.runner, target.label, uid);
503
- const unloaded = await pollUntil(deps, poll, async () => !(await launchdServiceInfo(deps.runner, target.label, uid)).loaded);
504
- if (!unloaded) {
765
+ const restarted = service.loaded;
766
+ // Stop the scheduler first. A helper that is already running either holds
767
+ // this lifecycle lock (so the controller could not enter) or is waiting to
768
+ // acquire it; bootout plus PID death prevents a post-stop resurrection.
769
+ const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
770
+ if (!maintenanceStopped.ok) {
771
+ return { ok: false, restarted, failure: maintenanceStopped.failure };
772
+ }
773
+ let mainStopProven = false;
774
+ if (service.loaded) {
775
+ const oldSources = await findInstances(target, deps);
776
+ const oldPids = uniquePids([
777
+ service.pid,
778
+ // A cleanly stopped manifest can outlive its process long enough for the OS
779
+ // to reuse that pid for unrelated work. It is historical evidence, not an
780
+ // ownership claim, so never wait for its recycled pid during restart.
781
+ ...oldSources
782
+ .filter((source) => source.health !== "stopped")
783
+ .map((source) => source.pid),
784
+ ]);
785
+ const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
786
+ if (!mainStopped.ok) {
787
+ const restoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
788
+ return {
789
+ ok: false,
790
+ restarted: true,
791
+ failure: restoreFailure === undefined
792
+ ? mainStopped.failure
793
+ : lifecycleFailure(mainStopped.failure, `scheduled maintenance restoration failed: ${maintenanceErrorMessage(restoreFailure)}`),
794
+ };
795
+ }
796
+ mainStopProven = true;
797
+ }
798
+ await whileStopped(mainStopProven);
799
+ const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
800
+ const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
801
+ if (!maintenanceLoaded) {
505
802
  return {
506
803
  ok: false,
507
- restarted: true,
508
- failure: lifecycleFailure(removed, "launchd still reports the previous service as loaded after bootout"),
804
+ restarted,
805
+ failure: lifecycleFailure(maintenanceBooted, "scheduled log maintenance did not report loaded"),
509
806
  };
510
807
  }
511
- const stopped = await pollUntil(deps, poll, async () => oldPids.every((pid) => !deps.isAlive(pid)));
512
- if (!stopped) {
808
+ const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
809
+ if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
810
+ return { ok: true, restarted };
811
+ }
812
+ const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
813
+ const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
814
+ return {
815
+ ok: false,
816
+ restarted,
817
+ failure: lifecycleFailure(booted, maintenanceRemoval.ok
818
+ ? "main service did not load; scheduled maintenance was unloaded"
819
+ : `main service did not load; scheduled maintenance cleanup failed: ${maintenanceErrorMessage(maintenanceRemoval.failure)}`),
820
+ };
821
+ }
822
+ async function unloadLaunchdService(label, service, ownedPids, deps, uid, poll, beforeLoadedBootout) {
823
+ const observedPids = new Set(uniquePids([service.pid, ...ownedPids]));
824
+ try {
825
+ await beforeLoadedBootout?.();
826
+ }
827
+ catch (error) {
513
828
  return {
514
829
  ok: false,
515
- restarted: true,
516
- failure: lifecycleFailure(removed, `previous worker pid(s) ${oldPids.join(", ")} remained alive after bootout`),
830
+ failure: { code: 1, stdout: "", stderr: maintenanceErrorMessage(error) },
517
831
  };
518
832
  }
519
- const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
520
- if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
521
- return { ok: true, restarted: true };
833
+ // Refresh immediately before bootout. The caller may have spent time on
834
+ // inventory or plist verification while KeepAlive replaced its entry PID.
835
+ let current = await launchdServiceInfo(deps.runner, label, uid);
836
+ if (current.pid !== undefined)
837
+ observedPids.add(current.pid);
838
+ let removed;
839
+ if (current.loaded) {
840
+ removed = await bootout(deps.runner, label, uid);
841
+ }
842
+ else {
843
+ removed = { code: 0, stdout: "", stderr: "" };
844
+ }
845
+ // Keep launchd state and process liveness in one predicate. In particular,
846
+ // retain every KeepAlive replacement pid exposed while bootout converges;
847
+ // proving only the entry snapshot dead is not a stopped-writer proof.
848
+ const stopped = await pollUntil(deps, poll, async () => {
849
+ current = await launchdServiceInfo(deps.runner, label, uid);
850
+ if (current.pid !== undefined)
851
+ observedPids.add(current.pid);
852
+ return !current.loaded && [...observedPids].every((pid) => !deps.isAlive(pid));
853
+ });
854
+ if (!stopped) {
855
+ const livePids = [...observedPids].filter((pid) => deps.isAlive(pid));
856
+ const detail = current.loaded
857
+ ? `launchd still reports ${label} loaded after bootout`
858
+ : `${label} pid(s) ${livePids.join(", ")} remained alive after bootout`;
859
+ return {
860
+ ok: false,
861
+ failure: lifecycleFailure(removed, detail),
862
+ };
522
863
  }
523
- return { ok: false, restarted: true, failure: booted };
864
+ return { ok: true };
865
+ }
866
+ function maintenancePathsForTarget(target) {
867
+ const label = deriveLaunchdMaintenanceLabel(target.label);
868
+ return { label, plistPath: resolve(target.paths.launchAgentsDir, `${label}.plist`) };
869
+ }
870
+ async function restoreMaintenanceDefinition(wasLoaded, maintenance, deps, uid, poll) {
871
+ if (!wasLoaded)
872
+ return undefined;
873
+ const restored = await bootstrap(deps.runner, maintenance.plistPath, uid);
874
+ const returned = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
875
+ if (returned)
876
+ return undefined;
877
+ return lifecycleFailure(restored, "scheduled maintenance did not return after the main stop failed");
524
878
  }
525
879
  async function findOwnershipConflict(target, deps, uid) {
526
880
  const [service, sources] = await Promise.all([
@@ -577,8 +931,26 @@ export async function stopBackground(target, deps, poll = DEFAULT_POLL) {
577
931
  }
578
932
  async function stopBackgroundUnlocked(target, deps, poll) {
579
933
  const uid = deps.getuid();
934
+ const maintenance = maintenancePathsForTarget(target);
580
935
  const existing = await findInstances(target, deps);
581
- const service = await launchdServiceInfo(deps.runner, target.label, uid);
936
+ const [service, maintenanceService] = await Promise.all([
937
+ launchdServiceInfo(deps.runner, target.label, uid),
938
+ launchdServiceInfo(deps.runner, maintenance.label, uid),
939
+ ]);
940
+ let maintenanceIntent;
941
+ try {
942
+ maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
943
+ }
944
+ catch (error) {
945
+ deps.stderr(ui.errorLine(`Failed to inspect pending launchd-log maintenance for ${target.label}.`));
946
+ deps.stderr(ui.style.dim(error instanceof Error ? error.message : String(error)) + "\n");
947
+ return 1;
948
+ }
949
+ if ((maintenanceIntent?.phase === "stopping" || maintenanceIntent?.phase === "restoring") && !service.loaded) {
950
+ deps.stderr(ui.errorLine(`Refusing to erase interrupted launchd-log maintenance for ${target.label} without stopped-writer proof.`));
951
+ deps.stderr(ui.hint("Confirm the prior worker is gone, then restore or remove the owner-private maintenance marker explicitly."));
952
+ return 1;
953
+ }
582
954
  const liveTracePids = uniquePids(existing
583
955
  .filter((source) => source.health !== "stopped" && source.pid !== undefined && deps.isAlive(source.pid))
584
956
  .map((source) => source.pid));
@@ -587,25 +959,53 @@ async function stopBackgroundUnlocked(target, deps, poll) {
587
959
  deps.stderr(ui.hint("The LaunchAgent plist was preserved. Stop the unmanaged process explicitly, then retry."));
588
960
  return 1;
589
961
  }
590
- const result = await bootout(deps.runner, target.label, uid);
591
- const unloaded = await pollUntil(deps, poll, async () => !(await launchdServiceInfo(deps.runner, target.label, uid)).loaded);
962
+ const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
963
+ if (!maintenanceStopped.ok) {
964
+ deps.stderr(ui.errorLine(`Failed to stop scheduled log maintenance for ${target.label}.`));
965
+ const detail = (maintenanceStopped.failure.stderr || maintenanceStopped.failure.stdout).trim();
966
+ if (detail.length > 0)
967
+ deps.stderr(ui.style.dim(detail) + "\n");
968
+ deps.stderr(ui.hint("Both LaunchAgent plists were preserved. Retry stop after the maintenance helper exits."));
969
+ return 1;
970
+ }
592
971
  const ownedPids = uniquePids([service.pid, ...liveTracePids]);
593
- const stopped = await pollUntil(deps, poll, async () => ownedPids.every((pid) => !deps.isAlive(pid)));
594
- if (!unloaded || !stopped) {
595
- deps.stderr(ui.errorLine(`Failed to prove ${target.label} stopped${result.code === 0 ? "" : ` (launchctl bootout exited ${result.code})`}.`));
596
- const detail = (result.stderr || result.stdout).trim();
972
+ const mainStopped = await unloadLaunchdService(target.label, service, ownedPids, deps, uid, poll, async () => {
973
+ const stoppedIntent = maintenanceIntent;
974
+ if (stoppedIntent?.phase === "stopped") {
975
+ maintenanceIntent = await deps.markLaunchdLogMaintenanceStopping(target.paths, stoppedIntent);
976
+ }
977
+ });
978
+ if (!mainStopped.ok) {
979
+ const result = mainStopped.failure;
980
+ const maintenanceRestoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
981
+ deps.stderr(ui.errorLine(`Failed to prove ${target.label} stopped${result?.code === undefined || result.code === 0 ? "" : ` (launchctl bootout exited ${result.code})`}.`));
982
+ const detail = (result?.stderr || result?.stdout || "").trim();
597
983
  if (detail.length > 0) {
598
984
  deps.stderr(ui.style.dim(detail) + "\n");
599
985
  }
600
- if (!unloaded)
601
- deps.stderr(ui.style.dim("launchd still reports the service as loaded.\n"));
602
- if (!stopped)
603
- deps.stderr(ui.style.dim(`Worker pid(s) ${ownedPids.join(", ")} are still alive.\n`));
604
- deps.stderr(ui.hint("The LaunchAgent plist was preserved so the service remains recoverable. Inspect status/logs and retry."));
986
+ if (maintenanceRestoreFailure !== undefined) {
987
+ deps.stderr(ui.style.yellow(`Scheduled log maintenance also could not be restored: ${maintenanceErrorMessage(maintenanceRestoreFailure)}`) + "\n");
988
+ }
989
+ deps.stderr(ui.hint("Both LaunchAgent plists were preserved so the service remains recoverable. Inspect status/logs and retry."));
990
+ return 1;
991
+ }
992
+ try {
993
+ // Explicit stop cancels any interrupted maintainer's restore authority
994
+ // only after both jobs and every observed pid are proven gone.
995
+ if (maintenanceIntent?.phase === "stopping" || maintenanceIntent?.phase === "restoring") {
996
+ maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
997
+ }
998
+ await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
999
+ }
1000
+ catch (error) {
1001
+ deps.stderr(ui.errorLine(`Failed to clear pending launchd-log maintenance for ${target.label}.`));
1002
+ deps.stderr(ui.style.dim(error instanceof Error ? error.message : String(error)) + "\n");
1003
+ deps.stderr(ui.hint("Both LaunchAgent plists were preserved; retry stop after the owner-private marker is inspectable."));
605
1004
  return 1;
606
1005
  }
607
- // Remove the plist only after both launchd ownership and process death are
608
- // proven. This prevents a failed stop from destroying its recovery handle.
1006
+ // Remove both definitions only after the helper and worker are unloaded and
1007
+ // every observed PID is dead. The helper can never resurrect a stopped job.
1008
+ await deps.rm(maintenance.plistPath);
609
1009
  await deps.rm(target.paths.plistPath);
610
1010
  for (const source of existing)
611
1011
  await maybeUnlinkDeadManifest(target, deps, source);
@@ -719,9 +1119,6 @@ async function findInstances(target, deps) {
719
1119
  })));
720
1120
  return matches.filter((entry) => entry.matches).map((entry) => entry.source);
721
1121
  }
722
- async function findInstance(target, deps) {
723
- return (await findInstances(target, deps))[0];
724
- }
725
1122
  async function maybeUnlinkDeadManifest(target, deps, existing) {
726
1123
  // Only clean up a manifest whose process is already gone; a worker that is
727
1124
  // still shutting down will mark its own manifest stopped.
@@ -1036,179 +1433,53 @@ export async function acquireFilesystemLifecycleLock(target, options = {}) {
1036
1433
  const pid = options.pid ?? process.pid;
1037
1434
  const now = options.now ?? (() => Date.now());
1038
1435
  const isProcessAlive = options.isProcessAlive ?? processIsAlive;
1039
- const incarnation = options.processIncarnation ?? await currentProcessIncarnation();
1040
- const isSameProcess = options.isSameProcessIncarnation ?? matchesProcessIncarnation;
1041
1436
  const ownerlessGraceMs = options.ownerlessGraceMs ?? 5 * 60_000;
1042
1437
  const randomToken = options.randomToken ?? randomUUID;
1043
- if (!Number.isSafeInteger(pid) || pid <= 0) {
1044
- throw new Error(`Lifecycle lock pid must be a positive safe integer; received ${String(pid)}.`);
1045
- }
1046
- if (!Number.isFinite(ownerlessGraceMs) || ownerlessGraceMs < 0) {
1047
- throw new Error("Lifecycle lock ownerless grace must be a non-negative finite duration.");
1048
- }
1438
+ validateOwnerPrivateLockInputs("Lifecycle lock", pid, ownerlessGraceMs);
1049
1439
  const managedRoot = dirname(target.paths.logDir);
1050
1440
  const locksDir = resolve(managedRoot, "locks");
1051
1441
  for (const path of [managedRoot, locksDir]) {
1052
1442
  await ensureOwnerPrivateLaunchdDirectory(path);
1053
1443
  }
1054
1444
  const lockDir = resolve(locksDir, `${target.label}.lock`);
1055
- const ownerPath = resolve(lockDir, "owner.json");
1056
- const token = `${pid}-${now()}-${randomToken()}`;
1057
- for (let attempt = 0; attempt < 4; attempt += 1) {
1058
- let createdIdentity;
1059
- try {
1060
- await mkdir(lockDir, { mode: 0o700 });
1061
- const created = await assertLifecycleLockDirectory(lockDir);
1062
- createdIdentity = lifecycleLockIdentity(created);
1063
- await writeFile(ownerPath, `${JSON.stringify({
1064
- pid,
1065
- token,
1066
- createdAt: new Date(now()).toISOString(),
1067
- incarnation,
1068
- })}\n`, {
1069
- encoding: "utf8",
1070
- flag: "wx",
1071
- mode: 0o600,
1072
- });
1073
- if (!(await sameLifecycleLockDirectory(lockDir, createdIdentity))) {
1074
- throw new Error(`Lifecycle lock ${lockDir} changed while its owner record was written.`);
1075
- }
1076
- const ownedIdentity = createdIdentity;
1077
- return async () => {
1078
- try {
1079
- if (!(await sameLifecycleLockDirectory(lockDir, ownedIdentity))) {
1080
- throw new Error(`Lifecycle lock ${lockDir} is no longer owned by this command.`);
1081
- }
1082
- const current = JSON.parse(await readFile(ownerPath, "utf8"));
1083
- if (current.token !== token) {
1084
- throw new Error(`Lifecycle lock ${lockDir} is no longer owned by this command.`);
1085
- }
1086
- const releasedPath = resolve(locksDir, `${target.label}.released-${now()}-${pid}-${randomToken()}`);
1087
- if (!(await sameLifecycleLockDirectory(lockDir, ownedIdentity))) {
1088
- throw new Error(`Lifecycle lock ${lockDir} changed during release.`);
1089
- }
1090
- await rename(lockDir, releasedPath);
1091
- const moved = await lstat(releasedPath);
1092
- if (!sameLifecycleLockIdentity(moved, ownedIdentity)) {
1093
- await rename(releasedPath, lockDir).catch(() => undefined);
1094
- throw new Error(`Lifecycle lock ${lockDir} changed during release.`);
1095
- }
1096
- await rm(releasedPath, { recursive: true, force: true });
1097
- }
1098
- catch (error) {
1099
- if (!isErrno(error, "ENOENT"))
1100
- throw error;
1101
- }
1102
- };
1103
- }
1104
- catch (error) {
1105
- if (createdIdentity !== undefined) {
1106
- await removeSameLifecycleLockDirectory(lockDir, createdIdentity).catch(() => undefined);
1107
- }
1108
- if (!isErrno(error, "EEXIST"))
1109
- throw error;
1110
- }
1111
- let observed;
1112
- try {
1113
- observed = await assertLifecycleLockDirectory(lockDir);
1114
- }
1115
- catch (error) {
1116
- if (isErrno(error, "ENOENT"))
1117
- continue;
1118
- return undefined;
1119
- }
1120
- const observedIdentity = lifecycleLockIdentity(observed);
1121
- const owner = await readLifecycleLockOwner(ownerPath);
1122
- if (!(await sameLifecycleLockDirectory(lockDir, observedIdentity)))
1123
- continue;
1124
- if (owner !== undefined) {
1125
- let sameOwner = true;
1126
- try {
1127
- sameOwner = owner.incarnation === undefined
1128
- ? isProcessAlive(owner.pid)
1129
- : await isSameProcess(owner.pid, owner.incarnation);
1130
- }
1131
- catch {
1132
- // A failed identity probe cannot authorize stealing another command's lock.
1133
- sameOwner = true;
1134
- }
1135
- if (sameOwner)
1445
+ const held = await acquireOwnerPrivateLock({
1446
+ path: lockDir,
1447
+ label: "Lifecycle lock",
1448
+ schemaTag: "mono-agent.filesystem-lifecycle-lock.v1",
1449
+ ownerlessGraceMs,
1450
+ maxAcquireAttempts: 4,
1451
+ pid,
1452
+ now,
1453
+ randomToken,
1454
+ ...(options.processIncarnation === undefined ? {} : { processIncarnation: options.processIncarnation }),
1455
+ ...(options.isSameProcessIncarnation === undefined
1456
+ ? {}
1457
+ : { isSameProcessIncarnation: options.isSameProcessIncarnation }),
1458
+ parseLegacyOwner: (record) => {
1459
+ if (typeof record.pid !== "number" || !Number.isSafeInteger(record.pid) || record.pid <= 0)
1136
1460
  return undefined;
1137
- }
1138
- if (owner === undefined) {
1139
- const lockAgeMs = now() - observed.mtimeMs;
1140
- // Another process may be between its atomic mkdir and owner.json write.
1141
- // Never steal that fresh ownerless lock; only recover abandoned debris.
1142
- if (lockAgeMs < ownerlessGraceMs)
1143
- return undefined;
1144
- }
1145
- if (!(await sameLifecycleLockDirectory(lockDir, observedIdentity)))
1146
- continue;
1147
- await options.beforeStaleLockRename?.();
1148
- const stalePath = resolve(locksDir, `${target.label}.stale-${now()}-${pid}-${randomToken()}`);
1149
- try {
1150
- await rename(lockDir, stalePath);
1151
- }
1152
- catch (error) {
1153
- if (isErrno(error, "ENOENT") || isErrno(error, "EEXIST"))
1154
- continue;
1155
- return undefined;
1156
- }
1157
- const moved = await lstat(stalePath);
1158
- if (!sameLifecycleLockIdentity(moved, observedIdentity)) {
1159
- // A contender replaced the path after our final identity check. Restore
1160
- // the directory we moved and fail closed; never delete its live lock.
1161
- await rename(stalePath, lockDir).catch(() => undefined);
1162
- return undefined;
1163
- }
1164
- await rm(stalePath, { recursive: true, force: true });
1165
- }
1166
- return undefined;
1167
- }
1168
- async function assertLifecycleLockDirectory(path) {
1169
- const details = await lstat(path);
1170
- if (!details.isDirectory() || details.isSymbolicLink()) {
1171
- throw new Error(`Lifecycle lock ${path} must be a real directory.`);
1172
- }
1173
- if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
1174
- throw new Error(`Lifecycle lock ${path} is not owned by the current user.`);
1175
- }
1176
- return details;
1177
- }
1178
- function lifecycleLockIdentity(details) {
1179
- return { dev: details.dev, ino: details.ino };
1180
- }
1181
- function sameLifecycleLockIdentity(details, identity) {
1182
- return details.dev === identity.dev && details.ino === identity.ino;
1183
- }
1184
- async function sameLifecycleLockDirectory(path, identity) {
1185
- try {
1186
- const details = await assertLifecycleLockDirectory(path);
1187
- return sameLifecycleLockIdentity(details, identity);
1188
- }
1189
- catch {
1190
- return false;
1191
- }
1192
- }
1193
- async function removeSameLifecycleLockDirectory(path, identity) {
1194
- if (await sameLifecycleLockDirectory(path, identity)) {
1195
- await rm(path, { recursive: true, force: true });
1196
- }
1197
- }
1198
- async function readLifecycleLockOwner(path) {
1199
- try {
1200
- const parsed = JSON.parse(await readFile(path, "utf8"));
1201
- const incarnation = processIncarnationFromJson(parsed.incarnation);
1202
- return typeof parsed.pid === "number" && Number.isSafeInteger(parsed.pid) && parsed.pid > 0
1203
- ? {
1204
- pid: parsed.pid,
1461
+ const incarnation = processIncarnationFromJson(record.incarnation);
1462
+ return {
1463
+ pid: record.pid,
1205
1464
  ...(incarnation === undefined ? {} : { incarnation }),
1206
- }
1207
- : undefined;
1208
- }
1209
- catch {
1210
- return undefined;
1211
- }
1465
+ };
1466
+ },
1467
+ // Permanent pre-v0.9.0 compatibility: a skipped-version upgrade can
1468
+ // encounter crash debris without incarnation identity indefinitely.
1469
+ // All owner records written since v0.9.0 use the stronger shared schema.
1470
+ allowCurrentUserLegacyOwnerMode: true,
1471
+ isLegacyProcessAlive: isProcessAlive,
1472
+ invalidOwner: "ownerless",
1473
+ livenessError: () => "assume-live",
1474
+ ...(options.beforeStaleLockRename === undefined
1475
+ ? {}
1476
+ : { beforeStaleRename: options.beforeStaleLockRename }),
1477
+ staleRace: "return",
1478
+ stalePath: ({ now: staleAt, pid: stalePid, token }) => resolve(locksDir, `${target.label}.stale-${staleAt}-${stalePid}-${token}`),
1479
+ releasedPath: ({ now: releasedAt, pid: ownerPid, token }) => resolve(locksDir, `${target.label}.released-${releasedAt}-${ownerPid}-${token}`),
1480
+ abandonedPath: ({ now: abandonedAt, pid: ownerPid, token }) => resolve(locksDir, `${target.label}.abandoned-${abandonedAt}-${ownerPid}-${token}`),
1481
+ });
1482
+ return held === undefined ? undefined : () => held.release();
1212
1483
  }
1213
1484
  function processIsAlive(pid) {
1214
1485
  try {