@mono-agent/agent-app 0.13.0 → 0.15.0

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 (192) hide show
  1. package/README.md +164 -37
  2. package/dist/adapter-send-tools-main.js +0 -1
  3. package/dist/adapter-send-tools-main.js.map +1 -1
  4. package/dist/adapter-send-tools.d.ts +8 -5
  5. package/dist/adapter-send-tools.d.ts.map +1 -1
  6. package/dist/adapter-send-tools.js +133 -287
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-config.d.ts.map +1 -1
  9. package/dist/app-config.js +3 -0
  10. package/dist/app-config.js.map +1 -1
  11. package/dist/app-controller-channels.d.ts +41 -7
  12. package/dist/app-controller-channels.d.ts.map +1 -1
  13. package/dist/app-controller-channels.js +4 -4
  14. package/dist/app-controller-channels.js.map +1 -1
  15. package/dist/app-controller-continuation.d.ts +43 -11
  16. package/dist/app-controller-continuation.d.ts.map +1 -1
  17. package/dist/app-controller-continuation.js +1 -5
  18. package/dist/app-controller-continuation.js.map +1 -1
  19. package/dist/app-controller-lifecycle.d.ts +34 -5
  20. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  21. package/dist/app-controller-lifecycle.js.map +1 -1
  22. package/dist/app-controller-maintenance.d.ts +46 -8
  23. package/dist/app-controller-maintenance.d.ts.map +1 -1
  24. package/dist/app-controller-maintenance.js +64 -1
  25. package/dist/app-controller-maintenance.js.map +1 -1
  26. package/dist/app-controller-memory-health.d.ts +39 -10
  27. package/dist/app-controller-memory-health.d.ts.map +1 -1
  28. package/dist/app-controller-memory-health.js.map +1 -1
  29. package/dist/app-controller-memory.d.ts +27 -5
  30. package/dist/app-controller-memory.d.ts.map +1 -1
  31. package/dist/app-controller-memory.js +0 -1
  32. package/dist/app-controller-memory.js.map +1 -1
  33. package/dist/app-controller-responder.d.ts +53 -7
  34. package/dist/app-controller-responder.d.ts.map +1 -1
  35. package/dist/app-controller-responder.js +5 -5
  36. package/dist/app-controller-responder.js.map +1 -1
  37. package/dist/app-controller-traceability.d.ts +63 -14
  38. package/dist/app-controller-traceability.d.ts.map +1 -1
  39. package/dist/app-controller-traceability.js.map +1 -1
  40. package/dist/app-controller-types.d.ts +63 -0
  41. package/dist/app-controller-types.d.ts.map +1 -0
  42. package/dist/app-controller-types.js +2 -0
  43. package/dist/app-controller-types.js.map +1 -0
  44. package/dist/app-controller-utils.d.ts +1 -1
  45. package/dist/app-controller-utils.d.ts.map +1 -1
  46. package/dist/app-controller-utils.js +1 -1
  47. package/dist/app-controller-utils.js.map +1 -1
  48. package/dist/app-controller.d.ts +5 -62
  49. package/dist/app-controller.d.ts.map +1 -1
  50. package/dist/app-controller.js +0 -6
  51. package/dist/app-controller.js.map +1 -1
  52. package/dist/background-environment.d.ts +1 -0
  53. package/dist/background-environment.d.ts.map +1 -1
  54. package/dist/background-environment.js +1 -0
  55. package/dist/background-environment.js.map +1 -1
  56. package/dist/background-lifecycle-utils.d.ts +27 -0
  57. package/dist/background-lifecycle-utils.d.ts.map +1 -0
  58. package/dist/background-lifecycle-utils.js +69 -0
  59. package/dist/background-lifecycle-utils.js.map +1 -0
  60. package/dist/background-log-maintenance.d.ts +6 -0
  61. package/dist/background-log-maintenance.d.ts.map +1 -0
  62. package/dist/background-log-maintenance.js +185 -0
  63. package/dist/background-log-maintenance.js.map +1 -0
  64. package/dist/background-runtime.d.ts +13 -2
  65. package/dist/background-runtime.d.ts.map +1 -1
  66. package/dist/background-runtime.js +27 -3
  67. package/dist/background-runtime.js.map +1 -1
  68. package/dist/background.d.ts +28 -5
  69. package/dist/background.d.ts.map +1 -1
  70. package/dist/background.js +229 -297
  71. package/dist/background.js.map +1 -1
  72. package/dist/channel-drivers/cron.js +8 -1
  73. package/dist/channel-drivers/cron.js.map +1 -1
  74. package/dist/channel-drivers/native-notify.d.ts +2 -0
  75. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  76. package/dist/channel-drivers/native-notify.js +14 -2
  77. package/dist/channel-drivers/native-notify.js.map +1 -1
  78. package/dist/channel-drivers/slack.d.ts.map +1 -1
  79. package/dist/channel-drivers/slack.js +49 -0
  80. package/dist/channel-drivers/slack.js.map +1 -1
  81. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  82. package/dist/channel-drivers/telegram.js +7 -18
  83. package/dist/channel-drivers/telegram.js.map +1 -1
  84. package/dist/channel-drivers/tui.d.ts.map +1 -1
  85. package/dist/channel-drivers/tui.js +8 -0
  86. package/dist/channel-drivers/tui.js.map +1 -1
  87. package/dist/channel-plugins.d.ts +2 -11
  88. package/dist/channel-plugins.d.ts.map +1 -1
  89. package/dist/channel-plugins.js +63 -60
  90. package/dist/channel-plugins.js.map +1 -1
  91. package/dist/channel-status-display.js +2 -6
  92. package/dist/channel-status-display.js.map +1 -1
  93. package/dist/channels.d.ts +5 -6
  94. package/dist/channels.d.ts.map +1 -1
  95. package/dist/channels.js +7 -5
  96. package/dist/channels.js.map +1 -1
  97. package/dist/cli-args.d.ts +8 -2
  98. package/dist/cli-args.d.ts.map +1 -1
  99. package/dist/cli-args.js +98 -27
  100. package/dist/cli-args.js.map +1 -1
  101. package/dist/cli-background-command.d.ts.map +1 -1
  102. package/dist/cli-background-command.js +90 -15
  103. package/dist/cli-background-command.js.map +1 -1
  104. package/dist/cli-commands.d.ts.map +1 -1
  105. package/dist/cli-commands.js +0 -14
  106. package/dist/cli-commands.js.map +1 -1
  107. package/dist/cli-help.d.ts.map +1 -1
  108. package/dist/cli-help.js +1 -7
  109. package/dist/cli-help.js.map +1 -1
  110. package/dist/cli-runs-command.d.ts +0 -3
  111. package/dist/cli-runs-command.d.ts.map +1 -1
  112. package/dist/cli-runs-command.js +1 -5
  113. package/dist/cli-runs-command.js.map +1 -1
  114. package/dist/cli-validate-config-command.d.ts.map +1 -1
  115. package/dist/cli-validate-config-command.js +12 -8
  116. package/dist/cli-validate-config-command.js.map +1 -1
  117. package/dist/config-reference.d.ts +4 -1
  118. package/dist/config-reference.d.ts.map +1 -1
  119. package/dist/config-reference.js +245 -69
  120. package/dist/config-reference.js.map +1 -1
  121. package/dist/configured-agent.d.ts +2 -8
  122. package/dist/configured-agent.d.ts.map +1 -1
  123. package/dist/configured-agent.js +3 -25
  124. package/dist/configured-agent.js.map +1 -1
  125. package/dist/consumer-contract.d.ts.map +1 -1
  126. package/dist/consumer-contract.js +2 -4
  127. package/dist/consumer-contract.js.map +1 -1
  128. package/dist/doctor-observability.d.ts +7 -0
  129. package/dist/doctor-observability.d.ts.map +1 -0
  130. package/dist/doctor-observability.js +80 -0
  131. package/dist/doctor-observability.js.map +1 -0
  132. package/dist/doctor-types.d.ts +17 -0
  133. package/dist/doctor-types.d.ts.map +1 -0
  134. package/dist/doctor-types.js +2 -0
  135. package/dist/doctor-types.js.map +1 -0
  136. package/dist/doctor.d.ts +2 -12
  137. package/dist/doctor.d.ts.map +1 -1
  138. package/dist/doctor.js +8 -108
  139. package/dist/doctor.js.map +1 -1
  140. package/dist/first-run-readiness.d.ts +1 -1
  141. package/dist/first-run-readiness.d.ts.map +1 -1
  142. package/dist/first-run-readiness.js +1 -1
  143. package/dist/first-run-readiness.js.map +1 -1
  144. package/dist/index.d.ts +0 -2
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +0 -1
  147. package/dist/index.js.map +1 -1
  148. package/dist/interaction-bridge.d.ts +5 -22
  149. package/dist/interaction-bridge.d.ts.map +1 -1
  150. package/dist/interaction-bridge.js +251 -141
  151. package/dist/interaction-bridge.js.map +1 -1
  152. package/dist/launchd.d.ts +33 -1
  153. package/dist/launchd.d.ts.map +1 -1
  154. package/dist/launchd.js +129 -2
  155. package/dist/launchd.js.map +1 -1
  156. package/dist/managed-web-logs.d.ts +16 -0
  157. package/dist/managed-web-logs.d.ts.map +1 -0
  158. package/dist/managed-web-logs.js +153 -0
  159. package/dist/managed-web-logs.js.map +1 -0
  160. package/dist/modules/catalog.js +1 -1
  161. package/dist/modules/catalog.js.map +1 -1
  162. package/dist/modules/known-tools.d.ts +1 -1
  163. package/dist/modules/known-tools.d.ts.map +1 -1
  164. package/dist/modules/known-tools.js +0 -3
  165. package/dist/modules/known-tools.js.map +1 -1
  166. package/dist/posted-reply-history.d.ts.map +1 -1
  167. package/dist/posted-reply-history.js +3 -0
  168. package/dist/posted-reply-history.js.map +1 -1
  169. package/dist/run-history.d.ts +6 -2
  170. package/dist/run-history.d.ts.map +1 -1
  171. package/dist/run-history.js +476 -96
  172. package/dist/run-history.js.map +1 -1
  173. package/dist/ui.js +1 -1
  174. package/dist/ui.js.map +1 -1
  175. package/dist/web-command.d.ts +4 -1
  176. package/dist/web-command.d.ts.map +1 -1
  177. package/dist/web-command.js +31 -1
  178. package/dist/web-command.js.map +1 -1
  179. package/dist/wizard/prompts.d.ts.map +1 -1
  180. package/dist/wizard/prompts.js +3 -4
  181. package/dist/wizard/prompts.js.map +1 -1
  182. package/dist/wizard/run.js +3 -3
  183. package/dist/wizard/run.js.map +1 -1
  184. package/package.json +16 -16
  185. package/schema/mono-agent.config.schema.json +282 -164
  186. package/skills/mono-agent-composer/SKILL.md +5 -5
  187. package/skills/mono-agent-composer/references/config-blueprint.md +6 -13
  188. package/skills/mono-agent-composer/references/discovery-questions.md +1 -1
  189. package/skills/mono-agent-composer/references/feature-coverage.md +7 -8
  190. package/skills/mono-agent-composer/references/package-map.md +5 -6
  191. package/skills/mono-agent-composer/references/playbooks.md +3 -3
  192. package/skills/mono-agent-composer/references/validation.md +21 -1
@@ -9,16 +9,18 @@ import { describeSensitiveDataExportWarning, phoenixAppBaseUrl, resolveAppTraceR
9
9
  import { formatChannelFactValue } from "./channel-fact-format.js";
10
10
  import { formatHumanChannelSections } from "./channel-status-display.js";
11
11
  import { hasCompletedManagedStartup } from "./managed-startup.js";
12
- import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
12
+ import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdManagedWorkerInfo, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
13
13
  import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
14
14
  import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, markLaunchdLogMaintenanceStopping, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
15
- import { ensureManagedBackgroundRuntime, MANAGED_BACKGROUND_WORKER_ENV, } from "./background-runtime.js";
15
+ import { ensureManagedBackgroundRuntime, inspectManagedRuntimeSourceIdentity, MANAGED_BACKGROUND_WORKER_ENV, verifyManagedRuntimeLaunch, } from "./background-runtime.js";
16
16
  import { backgroundSnapshotFromMetadata, captureBackgroundSnapshot, encodeBackgroundSnapshot, sameBackgroundSnapshot, } from "./background-snapshot.js";
17
17
  import { resolveConfiguredManagedRuntimePackages } from "./managed-runtime-packages.js";
18
18
  import { acquireManagedRuntimePublicationBarrier } from "./managed-runtime-publication.js";
19
19
  import { processIncarnationFromJson, } from "./process-incarnation.js";
20
20
  import { acquireOwnerPrivateLock, validateOwnerPrivateLockInputs } from "./owner-private-lock.js";
21
21
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
22
+ import { lifecycleFailure, maintenanceErrorMessage, pollUntil, uniquePids, unloadLaunchdService, } from "./background-lifecycle-utils.js";
23
+ import { maintainLaunchdLogsOperation, reportMaintenanceFailure, } from "./background-log-maintenance.js";
22
24
  import * as ui from "./ui.js";
23
25
  export { acquireBackgroundWorkerLease, backgroundWorkerLeasePath, } from "./background-worker-lease.js";
24
26
  export { LAUNCHD_LOG_MAX_BYTES, LAUNCHD_LOG_ROTATION_COUNT, } from "./launchd-logs.js";
@@ -68,6 +70,7 @@ export async function resolveInstanceTarget(input) {
68
70
  paths: launchdPathsFor(label),
69
71
  nodePath: process.execPath,
70
72
  cliPath: input.cliPath,
73
+ controllerCliPath: input.cliPath,
71
74
  // Bake an explicit --env-file (resolved absolute) into the plist so the
72
75
  // launchd worker loads the same env file the launcher did.
73
76
  ...(input.args.envFile === undefined ? {} : { envFile: resolve(cwd, input.args.envFile) }),
@@ -110,6 +113,7 @@ export function defaultBackgroundDeps() {
110
113
  return {
111
114
  runner: makeLaunchctlRunner(),
112
115
  getuid: () => process.getuid?.() ?? 0,
116
+ currentPid: () => process.pid,
113
117
  now: () => Date.now(),
114
118
  sleep: (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
115
119
  listRecordedRuns,
@@ -143,6 +147,8 @@ export function defaultBackgroundDeps() {
143
147
  }
144
148
  },
145
149
  ensureManagedRuntime: (input) => ensureManagedBackgroundRuntime(input),
150
+ inspectManagedRuntimeSourceIdentity,
151
+ verifyManagedRuntimeLaunch,
146
152
  resolveManagedRuntimePackages: (target) => resolveConfiguredManagedRuntimePackages({
147
153
  cwd: target.cwd,
148
154
  configPath: target.configPath,
@@ -172,6 +178,118 @@ export async function startBackground(target, deps, poll) {
172
178
  export async function restartBackground(target, deps, poll) {
173
179
  return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
174
180
  }
181
+ /**
182
+ * Private one-shot recovery controller invoked by the maintenance LaunchAgent.
183
+ * It authenticates the exact helper PID launchd owns, compares the inert source
184
+ * CLI identity with the strictly parsed loaded worker definition, and repairs
185
+ * only when the worker, snapshot, definition, or available source closure has
186
+ * drifted. A healthy worker remains serving while a replacement runtime is
187
+ * materialized.
188
+ */
189
+ export async function maintainLaunchdController(target, deps, options) {
190
+ const helper = maintenancePathsForTarget(target);
191
+ const uid = deps.getuid();
192
+ const helperService = await launchdServiceInfo(deps.runner, helper.label, uid);
193
+ const helperPid = deps.currentPid();
194
+ if (!helperService.loaded || helperService.pid !== helperPid || !deps.isAlive(helperPid)) {
195
+ reportMaintenanceFailure(target, deps, "authenticate the launchd-owned recovery controller", new Error(`launchd does not own this helper pid ${helperPid}`));
196
+ return 1;
197
+ }
198
+ const release = await deps.acquireLifecycleLock(target);
199
+ if (release === undefined) {
200
+ // A manual lifecycle command owns the label. The helper exits cleanly so
201
+ // that command can boot it out; StartInterval supplies the next retry.
202
+ return 0;
203
+ }
204
+ let maintainLogsOnly = false;
205
+ let resultCode = 1;
206
+ try {
207
+ const [worker, sources, desiredIdentity] = await Promise.all([
208
+ launchdManagedWorkerInfo(deps.runner, target.label, uid),
209
+ findInstances(target, deps),
210
+ deps.inspectManagedRuntimeSourceIdentity(target.cliPath),
211
+ ]);
212
+ let loadedIdentity;
213
+ if (worker.definition !== undefined) {
214
+ try {
215
+ loadedIdentity = await deps.verifyManagedRuntimeLaunch({
216
+ currentCliPath: worker.definition.cliPath,
217
+ launchProof: worker.definition.expectedManagedRuntimeLaunch,
218
+ });
219
+ }
220
+ catch {
221
+ // A malformed/unverified loaded closure is a recovery condition. Do not
222
+ // trust any identity or execute its advertised CLI.
223
+ }
224
+ }
225
+ const source = worker.pid === undefined
226
+ ? undefined
227
+ : sources.find((candidate) => candidate.pid === worker.pid);
228
+ const durableSnapshotStillMatches = await snapshotStillMatches(target, deps);
229
+ const workerHealthy = worker.loaded
230
+ && worker.pid !== undefined
231
+ && deps.isAlive(worker.pid)
232
+ && source !== undefined
233
+ && isReady(source, false)
234
+ && snapshotMetadataMatches(source, target.expectedSnapshot)
235
+ && durableSnapshotStillMatches;
236
+ const definitionMatches = worker.definition !== undefined
237
+ && managedWorkerDefinitionMatchesTarget(worker.definition, target);
238
+ const runtimeMatches = loadedIdentity !== undefined
239
+ && sameManagedRuntimeIdentity(loadedIdentity, desiredIdentity);
240
+ const needsRecovery = !workerHealthy
241
+ || !definitionMatches
242
+ || loadedIdentity === undefined
243
+ || (options.sourceAvailable && !runtimeMatches);
244
+ if (!needsRecovery) {
245
+ maintainLogsOnly = true;
246
+ resultCode = 0;
247
+ }
248
+ else {
249
+ if (!options.sourceAvailable) {
250
+ deps.stderr(ui.style.dim(`The original controller CLI is unavailable; recovering ${target.label} from the helper's private closure without claiming an upgrade.`) + "\n");
251
+ }
252
+ const recovered = await ensureBackgroundReadyUnlocked(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL, options.readinessPoll ?? DEFAULT_READINESS_POLL, { preserveMaintenanceService: true, preserveDefinitionsOnFailure: true });
253
+ resultCode = recovered.ok ? 0 : 1;
254
+ }
255
+ }
256
+ catch (error) {
257
+ reportMaintenanceFailure(target, deps, "reconcile the managed worker", error);
258
+ resultCode = 1;
259
+ }
260
+ finally {
261
+ await release().catch((error) => {
262
+ deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
263
+ });
264
+ }
265
+ return maintainLogsOnly
266
+ ? await maintainLaunchdLogs(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL)
267
+ : resultCode;
268
+ }
269
+ function managedWorkerDefinitionMatchesTarget(definition, target) {
270
+ return target.expectedSnapshot !== undefined
271
+ && definition.plistPath === target.paths.plistPath
272
+ && definition.nodePath === target.nodePath
273
+ && definition.configPath === target.configPath
274
+ && definition.cwd === target.cwd
275
+ && definition.envFile === target.envFile
276
+ && definition.stdoutPath === target.paths.stdoutPath
277
+ && definition.stderrPath === target.paths.stderrPath
278
+ && definition.expectedBackgroundSnapshot === encodeBackgroundSnapshot(target.expectedSnapshot)
279
+ && sameStringRecord(definition.environment, target.environment);
280
+ }
281
+ function sameManagedRuntimeIdentity(loaded, desired) {
282
+ return loaded.packageVersion === desired.packageVersion && loaded.cliSha256 === desired.cliSha256;
283
+ }
284
+ function sameStringRecord(left, right) {
285
+ const leftEntries = Object.entries(left).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
286
+ const rightEntries = Object.entries(right).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
287
+ return leftEntries.length === rightEntries.length
288
+ && leftEntries.every(([name, value], index) => {
289
+ const other = rightEntries[index];
290
+ return other?.[0] === name && other[1] === value;
291
+ });
292
+ }
175
293
  /**
176
294
  * Stop, perform the caller's stopped-worker mutation, and start again while
177
295
  * retaining one lifecycle lock. This closes the force-restart gap in which a
@@ -205,205 +323,7 @@ export async function forceRestartBackground(target, deps, whileStopped, poll) {
205
323
  * its own durable lifecycle intent after a prior maintainer died post-bootout.
206
324
  */
207
325
  export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_CONTROL_POLL) {
208
- const release = await deps.acquireLifecycleLock(target);
209
- if (release === undefined) {
210
- // Controller start/restart/stop owns the same lock. Skipping this periodic
211
- // pass is expected and avoids making launchd record a false maintenance
212
- // failure; the next interval retries from a fresh inventory.
213
- return 0;
214
- }
215
- try {
216
- const uid = deps.getuid();
217
- const service = await launchdServiceInfo(deps.runner, target.label, uid);
218
- let inspection;
219
- try {
220
- inspection = await deps.inspectLaunchdLogs(target.paths);
221
- }
222
- catch (error) {
223
- reportMaintenanceFailure(target, deps, "inspect launchd logs", error);
224
- return 1;
225
- }
226
- let maintenanceIntent;
227
- try {
228
- maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
229
- }
230
- catch (error) {
231
- reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", error);
232
- return 1;
233
- }
234
- if (inspection.pendingMaintenance && maintenanceIntent === undefined) {
235
- reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", new Error("The maintenance marker disappeared after inventory."));
236
- return 1;
237
- }
238
- if (!service.loaded && maintenanceIntent === undefined)
239
- return 0;
240
- let originalPlistIdentity;
241
- try {
242
- originalPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
243
- }
244
- catch (error) {
245
- reportMaintenanceFailure(target, deps, "verify the existing main LaunchAgent", error);
246
- return 1;
247
- }
248
- if (maintenanceIntent !== undefined
249
- && (maintenanceIntent.label !== target.label
250
- || maintenanceIntent.plistFingerprint !== originalPlistIdentity)) {
251
- reportMaintenanceFailure(target, deps, "authenticate durable launchd-log maintenance intent", new Error("The pending intent does not match the exact main LaunchAgent definition."));
252
- return 1;
253
- }
254
- if (maintenanceIntent?.phase === "stopping" && !service.loaded) {
255
- reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("The prior maintainer did not durably prove every old writer PID dead; refusing rotation."));
256
- return 1;
257
- }
258
- if (maintenanceIntent?.phase === "restoring") {
259
- const replacement = await launchdServiceInfo(deps.runner, target.label, uid);
260
- if (!replacement.loaded || replacement.pid === undefined || !deps.isAlive(replacement.pid)) {
261
- reportMaintenanceFailure(target, deps, "recover interrupted launchd-log restoration", new Error("The replacement writer identity was lost before live-worker proof; refusing stale rotation authority."));
262
- return 1;
263
- }
264
- try {
265
- await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
266
- }
267
- catch (error) {
268
- reportMaintenanceFailure(target, deps, "clear recovered launchd-log restoration intent", error);
269
- return 1;
270
- }
271
- return 0;
272
- }
273
- if (maintenanceIntent?.phase === "stopped" && service.loaded) {
274
- reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("launchd reports a writer loaded after durable stopped-writer proof; refusing rotation."));
275
- return 1;
276
- }
277
- if (!inspection.canMaintain) {
278
- deps.stderr(ui.errorLine(`Scheduled log maintenance refused unsafe paths for ${target.label}.`));
279
- for (const issue of inspection.issues)
280
- deps.stderr(ui.style.dim(issue) + "\n");
281
- return 1;
282
- }
283
- if (!inspection.needsMaintenance && maintenanceIntent === undefined)
284
- return 0;
285
- if (maintenanceIntent === undefined) {
286
- maintenanceIntent = {
287
- version: 1,
288
- phase: "stopping",
289
- label: target.label,
290
- plistFingerprint: originalPlistIdentity,
291
- };
292
- try {
293
- await deps.beginLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
294
- }
295
- catch (error) {
296
- reportMaintenanceFailure(target, deps, "publish durable launchd-log maintenance intent", error);
297
- return 1;
298
- }
299
- }
300
- if (maintenanceIntent.phase === "stopping") {
301
- const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid]), deps, uid, poll);
302
- if (!stopped.ok) {
303
- reportMaintenanceFailure(target, deps, "prove the launchd log writer stopped", stopped.failure);
304
- return 1;
305
- }
306
- try {
307
- maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
308
- }
309
- catch (error) {
310
- reportMaintenanceFailure(target, deps, "record durable stopped-writer proof", error);
311
- return 1;
312
- }
313
- }
314
- else {
315
- const current = await launchdServiceInfo(deps.runner, target.label, uid);
316
- if (current.loaded || (current.pid !== undefined && deps.isAlive(current.pid))) {
317
- reportMaintenanceFailure(target, deps, "recheck durable stopped-writer proof", new Error("launchd exposed a live writer before recovered rotation."));
318
- return 1;
319
- }
320
- }
321
- try {
322
- await deps.rotateStoppedLaunchdLogs(target.paths);
323
- // Close the pathname-replacement race before handing the definition to
324
- // launchctl. A changed/symlinked plist leaves the worker stopped.
325
- const currentPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
326
- if (currentPlistIdentity !== originalPlistIdentity) {
327
- throw new Error("The main LaunchAgent plist changed during stopped-writer maintenance.");
328
- }
329
- }
330
- catch (error) {
331
- reportMaintenanceFailure(target, deps, "commit bounded stopped-writer logs", error);
332
- return 1;
333
- }
334
- try {
335
- maintenanceIntent = await deps.markLaunchdLogMaintenanceRestoring(target.paths, maintenanceIntent);
336
- }
337
- catch (error) {
338
- reportMaintenanceFailure(target, deps, "invalidate stopped-writer proof before restoration", error);
339
- return 1;
340
- }
341
- const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
342
- const observedRestorePids = new Set();
343
- const running = await pollUntil(deps, poll, async () => {
344
- const current = await launchdServiceInfo(deps.runner, target.label, uid);
345
- if (current.pid !== undefined)
346
- observedRestorePids.add(current.pid);
347
- return current.loaded && current.pid !== undefined && deps.isAlive(current.pid);
348
- });
349
- if (!running) {
350
- const current = await launchdServiceInfo(deps.runner, target.label, uid);
351
- if (current.pid !== undefined)
352
- observedRestorePids.add(current.pid);
353
- const cleanedUp = await unloadLaunchdService(target.label, current, [...observedRestorePids], deps, uid, poll);
354
- reportMaintenanceFailure(target, deps, "restore the exact main LaunchAgent after rotation", lifecycleFailure(booted, "launchd did not expose a live replacement worker"));
355
- if (!cleanedUp.ok) {
356
- reportMaintenanceFailure(target, deps, "remove the failed replacement worker", cleanedUp.failure);
357
- }
358
- return 1;
359
- }
360
- try {
361
- const restoredPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
362
- if (restoredPlistIdentity !== originalPlistIdentity) {
363
- throw new Error("The main LaunchAgent plist changed while launchd restored the worker.");
364
- }
365
- }
366
- catch (error) {
367
- const restoredService = await launchdServiceInfo(deps.runner, target.label, uid);
368
- const stoppedAgain = await unloadLaunchdService(target.label, restoredService, uniquePids([restoredService.pid, ...observedRestorePids]), deps, uid, poll);
369
- reportMaintenanceFailure(target, deps, "prove the restored main LaunchAgent definition", error);
370
- if (!stoppedAgain.ok) {
371
- reportMaintenanceFailure(target, deps, "stop the worker after its definition changed", stoppedAgain.failure);
372
- }
373
- return 1;
374
- }
375
- try {
376
- await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
377
- }
378
- catch (error) {
379
- reportMaintenanceFailure(target, deps, "clear durable launchd-log maintenance intent", error);
380
- return 1;
381
- }
382
- return 0;
383
- }
384
- finally {
385
- await release().catch((error) => {
386
- deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
387
- });
388
- }
389
- }
390
- function reportMaintenanceFailure(target, deps, action, error) {
391
- deps.stderr(ui.errorLine(`Scheduled log maintenance could not ${action} for ${target.label}: ${maintenanceErrorMessage(error)}`));
392
- }
393
- function maintenanceErrorMessage(error) {
394
- if (error instanceof Error)
395
- return error.message;
396
- if (typeof error === "object" && error !== null) {
397
- const result = error;
398
- const detail = [result.stderr, result.stdout]
399
- .find((value) => typeof value === "string" && value.trim().length > 0)
400
- ?.trim();
401
- if (detail !== undefined)
402
- return detail;
403
- if (typeof result.code === "number")
404
- return `launchctl exited ${result.code}`;
405
- }
406
- return String(error);
326
+ return await maintainLaunchdLogsOperation(target, deps, poll);
407
327
  }
408
328
  /**
409
329
  * Ensure the canonical per-config LaunchAgent is running and return the fresh
@@ -427,7 +347,7 @@ export async function ensureBackgroundReady(target, deps, poll) {
427
347
  });
428
348
  }
429
349
  }
430
- async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll) {
350
+ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll, options = {}) {
431
351
  if (target.expectedSnapshot === undefined) {
432
352
  deps.stderr(ui.errorLine("Refusing to launch a managed worker without an approved background snapshot."));
433
353
  deps.stderr(ui.hint("No LaunchAgent changes were made. Retry from the wizard or `mono-agent start`."));
@@ -453,9 +373,9 @@ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readines
453
373
  deps.stderr(ui.errorLine(`Another runtime publication is already active for ${target.label}.`));
454
374
  return { ok: false, action: "start", reason: "ownership" };
455
375
  }
456
- return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier);
376
+ return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options);
457
377
  }
458
- async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier) {
378
+ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options) {
459
379
  let barrierReleased = false;
460
380
  const releaseBarrier = async () => {
461
381
  if (barrierReleased)
@@ -464,7 +384,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
464
384
  barrierReleased = true;
465
385
  };
466
386
  try {
467
- return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier);
387
+ return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options);
468
388
  }
469
389
  finally {
470
390
  if (!barrierReleased) {
@@ -474,7 +394,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
474
394
  }
475
395
  }
476
396
  }
477
- async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier) {
397
+ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options) {
478
398
  const uid = deps.getuid();
479
399
  let launchTarget;
480
400
  const runtimeStartedAt = deps.now();
@@ -560,7 +480,7 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
560
480
  // The replacement plist now contains the finalized-runtime proof. Let
561
481
  // launchd respawn only after the stopped-window commit is complete.
562
482
  await releaseBarrier();
563
- });
483
+ }, { preserveMaintenanceService: options.preserveMaintenanceService === true });
564
484
  }
565
485
  catch (error) {
566
486
  reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
@@ -582,13 +502,13 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
582
502
  }
583
503
  catch (error) {
584
504
  reportReadinessException(deps, error);
585
- const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
586
- reportReadinessCleanup(launchTarget, deps, stopped);
505
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
506
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
587
507
  return { ok: false, action, reason: "readiness" };
588
508
  }
589
509
  if (ready === undefined) {
590
510
  if (!(await snapshotStillMatches(launchTarget, deps))) {
591
- const stopped = await stopBackgroundUnlocked(launchTarget, deps, controlPoll);
511
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options) ? 0 : 1;
592
512
  reportSnapshotDrift(launchTarget, deps);
593
513
  if (stopped === 0) {
594
514
  deps.stderr(ui.style.dim("The drifted LaunchAgent was stopped before returning control.") + "\n");
@@ -599,8 +519,8 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
599
519
  return { ok: false, action, reason: "snapshot" };
600
520
  }
601
521
  reportTimeout(deps);
602
- const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
603
- reportReadinessCleanup(launchTarget, deps, stopped);
522
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
523
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
604
524
  return { ok: false, action, reason: "timeout" };
605
525
  }
606
526
  const completedAction = outcome.restarted ? "restarted" : "started";
@@ -633,6 +553,10 @@ async function writePlists(target, deps) {
633
553
  if (target.managedRuntimeLaunchProof === undefined) {
634
554
  throw new Error("A managed LaunchAgent requires a finalized runtime launch proof.");
635
555
  }
556
+ const agentPath = target.environment.PATH;
557
+ if (agentPath === undefined || agentPath.length === 0) {
558
+ throw new Error("A managed LaunchAgent recovery controller requires the durable worker PATH.");
559
+ }
636
560
  const maintenance = maintenancePathsForTarget(target);
637
561
  const mainXml = buildPlistXml({
638
562
  label: target.label,
@@ -652,6 +576,10 @@ async function writePlists(target, deps) {
652
576
  nodePath: target.nodePath,
653
577
  cliPath: target.cliPath,
654
578
  configPath: target.configPath,
579
+ controllerCliPath: target.controllerCliPath ?? target.cliPath,
580
+ agentCwd: target.cwd,
581
+ agentPath,
582
+ ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
655
583
  // The agent folder may be renamed or removed while its already-running
656
584
  // worker still owns log descriptors. Keep the scheduler's cwd in the
657
585
  // durable account state tree; its config argument is already absolute.
@@ -832,7 +760,7 @@ function sameFileSnapshot(left, right) {
832
760
  * before bootstrap so launchd cannot retain stale ProgramArguments or env from
833
761
  * the previous plist.
834
762
  */
835
- async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
763
+ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped, options = {}) {
836
764
  const maintenance = maintenancePathsForTarget(target);
837
765
  const [service, maintenanceService] = await Promise.all([
838
766
  launchdServiceInfo(deps.runner, target.label, uid),
@@ -842,9 +770,11 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
842
770
  // Stop the scheduler first. A helper that is already running either holds
843
771
  // this lifecycle lock (so the controller could not enter) or is waiting to
844
772
  // acquire it; bootout plus PID death prevents a post-stop resurrection.
845
- const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
846
- if (!maintenanceStopped.ok) {
847
- return { ok: false, restarted, failure: maintenanceStopped.failure };
773
+ if (options.preserveMaintenanceService !== true) {
774
+ const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
775
+ if (!maintenanceStopped.ok) {
776
+ return { ok: false, restarted, failure: maintenanceStopped.failure };
777
+ }
848
778
  }
849
779
  let mainStopProven = false;
850
780
  if (service.loaded) {
@@ -860,7 +790,9 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
860
790
  ]);
861
791
  const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
862
792
  if (!mainStopped.ok) {
863
- const restoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
793
+ const restoreFailure = options.preserveMaintenanceService === true
794
+ ? undefined
795
+ : await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
864
796
  return {
865
797
  ok: false,
866
798
  restarted: true,
@@ -872,19 +804,28 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
872
804
  mainStopProven = true;
873
805
  }
874
806
  await whileStopped(mainStopProven);
875
- const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
876
- const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
877
- if (!maintenanceLoaded) {
878
- return {
879
- ok: false,
880
- restarted,
881
- failure: lifecycleFailure(maintenanceBooted, "scheduled log maintenance did not report loaded"),
882
- };
807
+ if (options.preserveMaintenanceService !== true) {
808
+ const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
809
+ const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
810
+ if (!maintenanceLoaded) {
811
+ return {
812
+ ok: false,
813
+ restarted,
814
+ failure: lifecycleFailure(maintenanceBooted, "scheduled recovery controller did not report loaded"),
815
+ };
816
+ }
883
817
  }
884
818
  const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
885
819
  if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
886
820
  return { ok: true, restarted };
887
821
  }
822
+ if (options.preserveMaintenanceService === true) {
823
+ return {
824
+ ok: false,
825
+ restarted,
826
+ failure: lifecycleFailure(booted, "main service did not load; scheduled recovery remains loaded for retry"),
827
+ };
828
+ }
888
829
  const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
889
830
  const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
890
831
  return {
@@ -895,50 +836,6 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
895
836
  : `main service did not load; scheduled maintenance cleanup failed: ${maintenanceErrorMessage(maintenanceRemoval.failure)}`),
896
837
  };
897
838
  }
898
- async function unloadLaunchdService(label, service, ownedPids, deps, uid, poll, beforeLoadedBootout) {
899
- const observedPids = new Set(uniquePids([service.pid, ...ownedPids]));
900
- try {
901
- await beforeLoadedBootout?.();
902
- }
903
- catch (error) {
904
- return {
905
- ok: false,
906
- failure: { code: 1, stdout: "", stderr: maintenanceErrorMessage(error) },
907
- };
908
- }
909
- // Refresh immediately before bootout. The caller may have spent time on
910
- // inventory or plist verification while KeepAlive replaced its entry PID.
911
- let current = await launchdServiceInfo(deps.runner, label, uid);
912
- if (current.pid !== undefined)
913
- observedPids.add(current.pid);
914
- let removed;
915
- if (current.loaded) {
916
- removed = await bootout(deps.runner, label, uid);
917
- }
918
- else {
919
- removed = { code: 0, stdout: "", stderr: "" };
920
- }
921
- // Keep launchd state and process liveness in one predicate. In particular,
922
- // retain every KeepAlive replacement pid exposed while bootout converges;
923
- // proving only the entry snapshot dead is not a stopped-writer proof.
924
- const stopped = await pollUntil(deps, poll, async () => {
925
- current = await launchdServiceInfo(deps.runner, label, uid);
926
- if (current.pid !== undefined)
927
- observedPids.add(current.pid);
928
- return !current.loaded && [...observedPids].every((pid) => !deps.isAlive(pid));
929
- });
930
- if (!stopped) {
931
- const livePids = [...observedPids].filter((pid) => deps.isAlive(pid));
932
- const detail = current.loaded
933
- ? `launchd still reports ${label} loaded after bootout`
934
- : `${label} pid(s) ${livePids.join(", ")} remained alive after bootout`;
935
- return {
936
- ok: false,
937
- failure: lifecycleFailure(removed, detail),
938
- };
939
- }
940
- return { ok: true };
941
- }
942
839
  function maintenancePathsForTarget(target) {
943
840
  const label = deriveLaunchdMaintenanceLabel(target.label);
944
841
  return { label, plistPath: resolve(target.paths.launchAgentsDir, `${label}.plist`) };
@@ -969,26 +866,6 @@ async function findOwnershipConflict(target, deps, uid) {
969
866
  }
970
867
  return undefined;
971
868
  }
972
- async function pollUntil(deps, options, condition) {
973
- const deadline = deps.now() + options.timeoutMs;
974
- for (;;) {
975
- if (await condition())
976
- return true;
977
- if (deps.now() >= deadline)
978
- return false;
979
- await deps.sleep(options.intervalMs);
980
- }
981
- }
982
- function lifecycleFailure(result, detail) {
983
- return {
984
- code: result.code === 0 ? 1 : result.code,
985
- stdout: result.stdout,
986
- stderr: [result.stderr.trim(), detail].filter((value) => value.length > 0).join("\n"),
987
- };
988
- }
989
- function uniquePids(values) {
990
- return [...new Set(values.filter((value) => value !== undefined && value > 0))];
991
- }
992
869
  export async function stopBackground(target, deps, poll = DEFAULT_CONTROL_POLL) {
993
870
  const release = await deps.acquireLifecycleLock(target);
994
871
  if (release === undefined) {
@@ -1092,12 +969,24 @@ async function stopBackgroundUnlocked(target, deps, poll) {
1092
969
  return 0;
1093
970
  }
1094
971
  export async function statusBackground(target, deps, options = {}) {
1095
- const result = await deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs });
972
+ const [result, service] = await Promise.all([
973
+ deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs }),
974
+ launchdServiceInfo(deps.runner, target.label, deps.getuid()),
975
+ ]);
1096
976
  const classified = await Promise.all(result.sources.map(async (source) => ({
1097
977
  source,
1098
978
  matches: await matchesConfig(source, target.configPath),
1099
979
  })));
1100
- const current = classified.find((entry) => entry.matches)?.source;
980
+ const matchingSources = classified.filter((entry) => entry.matches).map((entry) => entry.source);
981
+ const recorded = service.pid === undefined
982
+ ? matchingSources[0]
983
+ : matchingSources.find((source) => source.pid === service.pid) ?? matchingSources[0];
984
+ const active = service.loaded
985
+ && service.pid !== undefined
986
+ && deps.isAlive(service.pid)
987
+ && recorded?.pid === service.pid
988
+ && recorded.health === "running";
989
+ const current = recorded === undefined || active ? recorded : inactiveStatusSource(recorded);
1101
990
  // Only surface other instances that are live or crashed — cleanly stopped
1102
991
  // manifests linger in the registry and would just be noise.
1103
992
  const others = classified
@@ -1106,11 +995,11 @@ export async function statusBackground(target, deps, options = {}) {
1106
995
  if (options.json === true) {
1107
996
  const instance = current === undefined ? null : await assembleInstanceStatus(current, target, deps);
1108
997
  deps.stdout(`${JSON.stringify({
1109
- ok: current?.health === "running",
998
+ ok: active,
1110
999
  instance,
1111
1000
  others: others.map(assembleOtherInstanceStatus),
1112
1001
  })}\n`);
1113
- return current?.health === "running" ? 0 : 1;
1002
+ return active ? 0 : 1;
1114
1003
  }
1115
1004
  if (current === undefined) {
1116
1005
  deps.stdout(ui.style.dim(`No running mono-agent instance for ${target.configPath}.`) + "\n");
@@ -1126,7 +1015,32 @@ export async function statusBackground(target, deps, options = {}) {
1126
1015
  deps.stdout(`${formatOtherInstance(source)}\n`);
1127
1016
  }
1128
1017
  }
1129
- return current?.health === "running" ? 0 : 1;
1018
+ return active ? 0 : 1;
1019
+ }
1020
+ function inactiveStatusSource(source) {
1021
+ const { transports: _staleTransports, pid: _stalePid, ...withoutLiveProcessFacts } = source;
1022
+ const metadata = source.metadata;
1023
+ const channels = metadata?.channels;
1024
+ const stoppedChannels = isPlainRecord(channels)
1025
+ ? Object.fromEntries(Object.entries(channels).map(([id, value]) => {
1026
+ if (!isPlainRecord(value) || value.kind !== "running")
1027
+ return [id, value];
1028
+ return [id, { kind: "stopped", reason: "instance is not running" }];
1029
+ }))
1030
+ : undefined;
1031
+ return {
1032
+ ...withoutLiveProcessFacts,
1033
+ health: "stopped",
1034
+ status: "stopped",
1035
+ ...(metadata === undefined
1036
+ ? {}
1037
+ : {
1038
+ metadata: {
1039
+ ...metadata,
1040
+ ...(stoppedChannels === undefined ? {} : { channels: stoppedChannels }),
1041
+ },
1042
+ }),
1043
+ };
1130
1044
  }
1131
1045
  /**
1132
1046
  * The machine-readable instance record for `status --json`. It mirrors the
@@ -1295,8 +1209,24 @@ function reportTimeout(deps) {
1295
1209
  deps.stderr(ui.errorLine("mono-agent did not report ready within the timeout."));
1296
1210
  deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
1297
1211
  }
1298
- async function cleanUpUnreadyBackground(target, deps, poll) {
1212
+ async function cleanUpUnreadyBackground(target, deps, poll, options = {}) {
1299
1213
  try {
1214
+ if (options.preserveDefinitionsOnFailure === true) {
1215
+ const uid = deps.getuid();
1216
+ const [service, sources] = await Promise.all([
1217
+ launchdServiceInfo(deps.runner, target.label, uid),
1218
+ findInstances(target, deps),
1219
+ ]);
1220
+ const liveTracePids = uniquePids(sources
1221
+ .filter((source) => source.health !== "stopped"
1222
+ && source.pid !== undefined
1223
+ && deps.isAlive(source.pid))
1224
+ .map((source) => source.pid));
1225
+ if (liveTracePids.some((pid) => pid !== service.pid))
1226
+ return false;
1227
+ const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid, ...liveTracePids]), deps, uid, poll);
1228
+ return stopped.ok;
1229
+ }
1300
1230
  return await stopBackgroundUnlocked(target, { ...deps, stdout: () => undefined }, poll) === 0;
1301
1231
  }
1302
1232
  catch (error) {
@@ -1304,9 +1234,11 @@ async function cleanUpUnreadyBackground(target, deps, poll) {
1304
1234
  return false;
1305
1235
  }
1306
1236
  }
1307
- function reportReadinessCleanup(target, deps, stopped) {
1237
+ function reportReadinessCleanup(target, deps, stopped, options = {}) {
1308
1238
  deps.stderr(stopped
1309
- ? ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
1239
+ ? options.preserveDefinitionsOnFailure === true
1240
+ ? ui.style.dim("The unready main LaunchAgent was stopped; both definitions and the scheduled recovery controller remain for retry.") + "\n"
1241
+ : ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
1310
1242
  : ui.style.yellow("The unready LaunchAgent could not be proven stopped; a worker or maintenance helper may still be running.") + "\n");
1311
1243
  reportLaunchRecovery(target, deps);
1312
1244
  }