@mono-agent/agent-app 0.13.0 → 0.14.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 (104) hide show
  1. package/README.md +163 -32
  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-controller-channels.d.ts.map +1 -1
  9. package/dist/app-controller-channels.js +2 -1
  10. package/dist/app-controller-channels.js.map +1 -1
  11. package/dist/app-controller-continuation.d.ts.map +1 -1
  12. package/dist/app-controller-continuation.js +1 -5
  13. package/dist/app-controller-continuation.js.map +1 -1
  14. package/dist/app-controller-maintenance.d.ts +2 -1
  15. package/dist/app-controller-maintenance.d.ts.map +1 -1
  16. package/dist/app-controller-maintenance.js +64 -1
  17. package/dist/app-controller-maintenance.js.map +1 -1
  18. package/dist/app-controller-responder.d.ts.map +1 -1
  19. package/dist/app-controller-responder.js +4 -1
  20. package/dist/app-controller-responder.js.map +1 -1
  21. package/dist/app-controller-utils.js +1 -1
  22. package/dist/app-controller-utils.js.map +1 -1
  23. package/dist/background-environment.d.ts +1 -0
  24. package/dist/background-environment.d.ts.map +1 -1
  25. package/dist/background-environment.js +1 -0
  26. package/dist/background-environment.js.map +1 -1
  27. package/dist/background-runtime.d.ts +13 -2
  28. package/dist/background-runtime.d.ts.map +1 -1
  29. package/dist/background-runtime.js +27 -3
  30. package/dist/background-runtime.js.map +1 -1
  31. package/dist/background.d.ts +26 -1
  32. package/dist/background.d.ts.map +1 -1
  33. package/dist/background.js +226 -34
  34. package/dist/background.js.map +1 -1
  35. package/dist/channel-drivers/cron.js +8 -1
  36. package/dist/channel-drivers/cron.js.map +1 -1
  37. package/dist/channel-drivers/native-notify.d.ts +2 -0
  38. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  39. package/dist/channel-drivers/native-notify.js +14 -2
  40. package/dist/channel-drivers/native-notify.js.map +1 -1
  41. package/dist/channel-drivers/slack.d.ts.map +1 -1
  42. package/dist/channel-drivers/slack.js +49 -0
  43. package/dist/channel-drivers/slack.js.map +1 -1
  44. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  45. package/dist/channel-drivers/telegram.js +7 -18
  46. package/dist/channel-drivers/telegram.js.map +1 -1
  47. package/dist/channel-drivers/tui.d.ts.map +1 -1
  48. package/dist/channel-drivers/tui.js +8 -0
  49. package/dist/channel-drivers/tui.js.map +1 -1
  50. package/dist/cli-args.d.ts +8 -2
  51. package/dist/cli-args.d.ts.map +1 -1
  52. package/dist/cli-args.js +48 -27
  53. package/dist/cli-args.js.map +1 -1
  54. package/dist/cli-background-command.d.ts.map +1 -1
  55. package/dist/cli-background-command.js +89 -14
  56. package/dist/cli-background-command.js.map +1 -1
  57. package/dist/cli-commands.d.ts.map +1 -1
  58. package/dist/cli-commands.js +0 -14
  59. package/dist/cli-commands.js.map +1 -1
  60. package/dist/cli-help.d.ts.map +1 -1
  61. package/dist/cli-help.js +1 -7
  62. package/dist/cli-help.js.map +1 -1
  63. package/dist/cli-runs-command.d.ts +0 -3
  64. package/dist/cli-runs-command.d.ts.map +1 -1
  65. package/dist/cli-runs-command.js +1 -5
  66. package/dist/cli-runs-command.js.map +1 -1
  67. package/dist/config-reference.d.ts.map +1 -1
  68. package/dist/config-reference.js +3 -4
  69. package/dist/config-reference.js.map +1 -1
  70. package/dist/doctor.js +1 -2
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/interaction-bridge.d.ts +5 -22
  73. package/dist/interaction-bridge.d.ts.map +1 -1
  74. package/dist/interaction-bridge.js +251 -141
  75. package/dist/interaction-bridge.js.map +1 -1
  76. package/dist/launchd.d.ts +33 -1
  77. package/dist/launchd.d.ts.map +1 -1
  78. package/dist/launchd.js +129 -2
  79. package/dist/launchd.js.map +1 -1
  80. package/dist/modules/catalog.js +1 -1
  81. package/dist/modules/catalog.js.map +1 -1
  82. package/dist/modules/known-tools.d.ts +1 -1
  83. package/dist/modules/known-tools.d.ts.map +1 -1
  84. package/dist/modules/known-tools.js +0 -3
  85. package/dist/modules/known-tools.js.map +1 -1
  86. package/dist/run-history.d.ts +6 -2
  87. package/dist/run-history.d.ts.map +1 -1
  88. package/dist/run-history.js +476 -96
  89. package/dist/run-history.js.map +1 -1
  90. package/dist/ui.js +1 -1
  91. package/dist/ui.js.map +1 -1
  92. package/dist/wizard/prompts.d.ts.map +1 -1
  93. package/dist/wizard/prompts.js +3 -4
  94. package/dist/wizard/prompts.js.map +1 -1
  95. package/dist/wizard/run.js +3 -3
  96. package/dist/wizard/run.js.map +1 -1
  97. package/package.json +15 -15
  98. package/schema/mono-agent.config.schema.json +2 -2
  99. package/skills/mono-agent-composer/SKILL.md +5 -5
  100. package/skills/mono-agent-composer/references/config-blueprint.md +2 -2
  101. package/skills/mono-agent-composer/references/feature-coverage.md +3 -3
  102. package/skills/mono-agent-composer/references/package-map.md +1 -1
  103. package/skills/mono-agent-composer/references/playbooks.md +1 -1
  104. package/skills/mono-agent-composer/references/validation.md +20 -0
@@ -9,10 +9,10 @@ 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";
@@ -68,6 +68,7 @@ export async function resolveInstanceTarget(input) {
68
68
  paths: launchdPathsFor(label),
69
69
  nodePath: process.execPath,
70
70
  cliPath: input.cliPath,
71
+ controllerCliPath: input.cliPath,
71
72
  // Bake an explicit --env-file (resolved absolute) into the plist so the
72
73
  // launchd worker loads the same env file the launcher did.
73
74
  ...(input.args.envFile === undefined ? {} : { envFile: resolve(cwd, input.args.envFile) }),
@@ -110,6 +111,7 @@ export function defaultBackgroundDeps() {
110
111
  return {
111
112
  runner: makeLaunchctlRunner(),
112
113
  getuid: () => process.getuid?.() ?? 0,
114
+ currentPid: () => process.pid,
113
115
  now: () => Date.now(),
114
116
  sleep: (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
115
117
  listRecordedRuns,
@@ -143,6 +145,8 @@ export function defaultBackgroundDeps() {
143
145
  }
144
146
  },
145
147
  ensureManagedRuntime: (input) => ensureManagedBackgroundRuntime(input),
148
+ inspectManagedRuntimeSourceIdentity,
149
+ verifyManagedRuntimeLaunch,
146
150
  resolveManagedRuntimePackages: (target) => resolveConfiguredManagedRuntimePackages({
147
151
  cwd: target.cwd,
148
152
  configPath: target.configPath,
@@ -172,6 +176,118 @@ export async function startBackground(target, deps, poll) {
172
176
  export async function restartBackground(target, deps, poll) {
173
177
  return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
174
178
  }
179
+ /**
180
+ * Private one-shot recovery controller invoked by the maintenance LaunchAgent.
181
+ * It authenticates the exact helper PID launchd owns, compares the inert source
182
+ * CLI identity with the strictly parsed loaded worker definition, and repairs
183
+ * only when the worker, snapshot, definition, or available source closure has
184
+ * drifted. A healthy worker remains serving while a replacement runtime is
185
+ * materialized.
186
+ */
187
+ export async function maintainLaunchdController(target, deps, options) {
188
+ const helper = maintenancePathsForTarget(target);
189
+ const uid = deps.getuid();
190
+ const helperService = await launchdServiceInfo(deps.runner, helper.label, uid);
191
+ const helperPid = deps.currentPid();
192
+ if (!helperService.loaded || helperService.pid !== helperPid || !deps.isAlive(helperPid)) {
193
+ reportMaintenanceFailure(target, deps, "authenticate the launchd-owned recovery controller", new Error(`launchd does not own this helper pid ${helperPid}`));
194
+ return 1;
195
+ }
196
+ const release = await deps.acquireLifecycleLock(target);
197
+ if (release === undefined) {
198
+ // A manual lifecycle command owns the label. The helper exits cleanly so
199
+ // that command can boot it out; StartInterval supplies the next retry.
200
+ return 0;
201
+ }
202
+ let maintainLogsOnly = false;
203
+ let resultCode = 1;
204
+ try {
205
+ const [worker, sources, desiredIdentity] = await Promise.all([
206
+ launchdManagedWorkerInfo(deps.runner, target.label, uid),
207
+ findInstances(target, deps),
208
+ deps.inspectManagedRuntimeSourceIdentity(target.cliPath),
209
+ ]);
210
+ let loadedIdentity;
211
+ if (worker.definition !== undefined) {
212
+ try {
213
+ loadedIdentity = await deps.verifyManagedRuntimeLaunch({
214
+ currentCliPath: worker.definition.cliPath,
215
+ launchProof: worker.definition.expectedManagedRuntimeLaunch,
216
+ });
217
+ }
218
+ catch {
219
+ // A malformed/unverified loaded closure is a recovery condition. Do not
220
+ // trust any identity or execute its advertised CLI.
221
+ }
222
+ }
223
+ const source = worker.pid === undefined
224
+ ? undefined
225
+ : sources.find((candidate) => candidate.pid === worker.pid);
226
+ const durableSnapshotStillMatches = await snapshotStillMatches(target, deps);
227
+ const workerHealthy = worker.loaded
228
+ && worker.pid !== undefined
229
+ && deps.isAlive(worker.pid)
230
+ && source !== undefined
231
+ && isReady(source, false)
232
+ && snapshotMetadataMatches(source, target.expectedSnapshot)
233
+ && durableSnapshotStillMatches;
234
+ const definitionMatches = worker.definition !== undefined
235
+ && managedWorkerDefinitionMatchesTarget(worker.definition, target);
236
+ const runtimeMatches = loadedIdentity !== undefined
237
+ && sameManagedRuntimeIdentity(loadedIdentity, desiredIdentity);
238
+ const needsRecovery = !workerHealthy
239
+ || !definitionMatches
240
+ || loadedIdentity === undefined
241
+ || (options.sourceAvailable && !runtimeMatches);
242
+ if (!needsRecovery) {
243
+ maintainLogsOnly = true;
244
+ resultCode = 0;
245
+ }
246
+ else {
247
+ if (!options.sourceAvailable) {
248
+ 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");
249
+ }
250
+ const recovered = await ensureBackgroundReadyUnlocked(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL, options.readinessPoll ?? DEFAULT_READINESS_POLL, { preserveMaintenanceService: true, preserveDefinitionsOnFailure: true });
251
+ resultCode = recovered.ok ? 0 : 1;
252
+ }
253
+ }
254
+ catch (error) {
255
+ reportMaintenanceFailure(target, deps, "reconcile the managed worker", error);
256
+ resultCode = 1;
257
+ }
258
+ finally {
259
+ await release().catch((error) => {
260
+ deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
261
+ });
262
+ }
263
+ return maintainLogsOnly
264
+ ? await maintainLaunchdLogs(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL)
265
+ : resultCode;
266
+ }
267
+ function managedWorkerDefinitionMatchesTarget(definition, target) {
268
+ return target.expectedSnapshot !== undefined
269
+ && definition.plistPath === target.paths.plistPath
270
+ && definition.nodePath === target.nodePath
271
+ && definition.configPath === target.configPath
272
+ && definition.cwd === target.cwd
273
+ && definition.envFile === target.envFile
274
+ && definition.stdoutPath === target.paths.stdoutPath
275
+ && definition.stderrPath === target.paths.stderrPath
276
+ && definition.expectedBackgroundSnapshot === encodeBackgroundSnapshot(target.expectedSnapshot)
277
+ && sameStringRecord(definition.environment, target.environment);
278
+ }
279
+ function sameManagedRuntimeIdentity(loaded, desired) {
280
+ return loaded.packageVersion === desired.packageVersion && loaded.cliSha256 === desired.cliSha256;
281
+ }
282
+ function sameStringRecord(left, right) {
283
+ const leftEntries = Object.entries(left).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
284
+ const rightEntries = Object.entries(right).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
285
+ return leftEntries.length === rightEntries.length
286
+ && leftEntries.every(([name, value], index) => {
287
+ const other = rightEntries[index];
288
+ return other?.[0] === name && other[1] === value;
289
+ });
290
+ }
175
291
  /**
176
292
  * Stop, perform the caller's stopped-worker mutation, and start again while
177
293
  * retaining one lifecycle lock. This closes the force-restart gap in which a
@@ -427,7 +543,7 @@ export async function ensureBackgroundReady(target, deps, poll) {
427
543
  });
428
544
  }
429
545
  }
430
- async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll) {
546
+ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll, options = {}) {
431
547
  if (target.expectedSnapshot === undefined) {
432
548
  deps.stderr(ui.errorLine("Refusing to launch a managed worker without an approved background snapshot."));
433
549
  deps.stderr(ui.hint("No LaunchAgent changes were made. Retry from the wizard or `mono-agent start`."));
@@ -453,9 +569,9 @@ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readines
453
569
  deps.stderr(ui.errorLine(`Another runtime publication is already active for ${target.label}.`));
454
570
  return { ok: false, action: "start", reason: "ownership" };
455
571
  }
456
- return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier);
572
+ return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options);
457
573
  }
458
- async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier) {
574
+ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options) {
459
575
  let barrierReleased = false;
460
576
  const releaseBarrier = async () => {
461
577
  if (barrierReleased)
@@ -464,7 +580,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
464
580
  barrierReleased = true;
465
581
  };
466
582
  try {
467
- return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier);
583
+ return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options);
468
584
  }
469
585
  finally {
470
586
  if (!barrierReleased) {
@@ -474,7 +590,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
474
590
  }
475
591
  }
476
592
  }
477
- async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier) {
593
+ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options) {
478
594
  const uid = deps.getuid();
479
595
  let launchTarget;
480
596
  const runtimeStartedAt = deps.now();
@@ -560,7 +676,7 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
560
676
  // The replacement plist now contains the finalized-runtime proof. Let
561
677
  // launchd respawn only after the stopped-window commit is complete.
562
678
  await releaseBarrier();
563
- });
679
+ }, { preserveMaintenanceService: options.preserveMaintenanceService === true });
564
680
  }
565
681
  catch (error) {
566
682
  reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
@@ -582,13 +698,13 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
582
698
  }
583
699
  catch (error) {
584
700
  reportReadinessException(deps, error);
585
- const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
586
- reportReadinessCleanup(launchTarget, deps, stopped);
701
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
702
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
587
703
  return { ok: false, action, reason: "readiness" };
588
704
  }
589
705
  if (ready === undefined) {
590
706
  if (!(await snapshotStillMatches(launchTarget, deps))) {
591
- const stopped = await stopBackgroundUnlocked(launchTarget, deps, controlPoll);
707
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options) ? 0 : 1;
592
708
  reportSnapshotDrift(launchTarget, deps);
593
709
  if (stopped === 0) {
594
710
  deps.stderr(ui.style.dim("The drifted LaunchAgent was stopped before returning control.") + "\n");
@@ -599,8 +715,8 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
599
715
  return { ok: false, action, reason: "snapshot" };
600
716
  }
601
717
  reportTimeout(deps);
602
- const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
603
- reportReadinessCleanup(launchTarget, deps, stopped);
718
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
719
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
604
720
  return { ok: false, action, reason: "timeout" };
605
721
  }
606
722
  const completedAction = outcome.restarted ? "restarted" : "started";
@@ -633,6 +749,10 @@ async function writePlists(target, deps) {
633
749
  if (target.managedRuntimeLaunchProof === undefined) {
634
750
  throw new Error("A managed LaunchAgent requires a finalized runtime launch proof.");
635
751
  }
752
+ const agentPath = target.environment.PATH;
753
+ if (agentPath === undefined || agentPath.length === 0) {
754
+ throw new Error("A managed LaunchAgent recovery controller requires the durable worker PATH.");
755
+ }
636
756
  const maintenance = maintenancePathsForTarget(target);
637
757
  const mainXml = buildPlistXml({
638
758
  label: target.label,
@@ -652,6 +772,10 @@ async function writePlists(target, deps) {
652
772
  nodePath: target.nodePath,
653
773
  cliPath: target.cliPath,
654
774
  configPath: target.configPath,
775
+ controllerCliPath: target.controllerCliPath ?? target.cliPath,
776
+ agentCwd: target.cwd,
777
+ agentPath,
778
+ ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
655
779
  // The agent folder may be renamed or removed while its already-running
656
780
  // worker still owns log descriptors. Keep the scheduler's cwd in the
657
781
  // durable account state tree; its config argument is already absolute.
@@ -832,7 +956,7 @@ function sameFileSnapshot(left, right) {
832
956
  * before bootstrap so launchd cannot retain stale ProgramArguments or env from
833
957
  * the previous plist.
834
958
  */
835
- async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
959
+ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped, options = {}) {
836
960
  const maintenance = maintenancePathsForTarget(target);
837
961
  const [service, maintenanceService] = await Promise.all([
838
962
  launchdServiceInfo(deps.runner, target.label, uid),
@@ -842,9 +966,11 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
842
966
  // Stop the scheduler first. A helper that is already running either holds
843
967
  // this lifecycle lock (so the controller could not enter) or is waiting to
844
968
  // 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 };
969
+ if (options.preserveMaintenanceService !== true) {
970
+ const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
971
+ if (!maintenanceStopped.ok) {
972
+ return { ok: false, restarted, failure: maintenanceStopped.failure };
973
+ }
848
974
  }
849
975
  let mainStopProven = false;
850
976
  if (service.loaded) {
@@ -860,7 +986,9 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
860
986
  ]);
861
987
  const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
862
988
  if (!mainStopped.ok) {
863
- const restoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
989
+ const restoreFailure = options.preserveMaintenanceService === true
990
+ ? undefined
991
+ : await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
864
992
  return {
865
993
  ok: false,
866
994
  restarted: true,
@@ -872,19 +1000,28 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
872
1000
  mainStopProven = true;
873
1001
  }
874
1002
  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
- };
1003
+ if (options.preserveMaintenanceService !== true) {
1004
+ const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
1005
+ const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
1006
+ if (!maintenanceLoaded) {
1007
+ return {
1008
+ ok: false,
1009
+ restarted,
1010
+ failure: lifecycleFailure(maintenanceBooted, "scheduled recovery controller did not report loaded"),
1011
+ };
1012
+ }
883
1013
  }
884
1014
  const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
885
1015
  if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
886
1016
  return { ok: true, restarted };
887
1017
  }
1018
+ if (options.preserveMaintenanceService === true) {
1019
+ return {
1020
+ ok: false,
1021
+ restarted,
1022
+ failure: lifecycleFailure(booted, "main service did not load; scheduled recovery remains loaded for retry"),
1023
+ };
1024
+ }
888
1025
  const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
889
1026
  const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
890
1027
  return {
@@ -1092,12 +1229,24 @@ async function stopBackgroundUnlocked(target, deps, poll) {
1092
1229
  return 0;
1093
1230
  }
1094
1231
  export async function statusBackground(target, deps, options = {}) {
1095
- const result = await deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs });
1232
+ const [result, service] = await Promise.all([
1233
+ deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs }),
1234
+ launchdServiceInfo(deps.runner, target.label, deps.getuid()),
1235
+ ]);
1096
1236
  const classified = await Promise.all(result.sources.map(async (source) => ({
1097
1237
  source,
1098
1238
  matches: await matchesConfig(source, target.configPath),
1099
1239
  })));
1100
- const current = classified.find((entry) => entry.matches)?.source;
1240
+ const matchingSources = classified.filter((entry) => entry.matches).map((entry) => entry.source);
1241
+ const recorded = service.pid === undefined
1242
+ ? matchingSources[0]
1243
+ : matchingSources.find((source) => source.pid === service.pid) ?? matchingSources[0];
1244
+ const active = service.loaded
1245
+ && service.pid !== undefined
1246
+ && deps.isAlive(service.pid)
1247
+ && recorded?.pid === service.pid
1248
+ && recorded.health === "running";
1249
+ const current = recorded === undefined || active ? recorded : inactiveStatusSource(recorded);
1101
1250
  // Only surface other instances that are live or crashed — cleanly stopped
1102
1251
  // manifests linger in the registry and would just be noise.
1103
1252
  const others = classified
@@ -1106,11 +1255,11 @@ export async function statusBackground(target, deps, options = {}) {
1106
1255
  if (options.json === true) {
1107
1256
  const instance = current === undefined ? null : await assembleInstanceStatus(current, target, deps);
1108
1257
  deps.stdout(`${JSON.stringify({
1109
- ok: current?.health === "running",
1258
+ ok: active,
1110
1259
  instance,
1111
1260
  others: others.map(assembleOtherInstanceStatus),
1112
1261
  })}\n`);
1113
- return current?.health === "running" ? 0 : 1;
1262
+ return active ? 0 : 1;
1114
1263
  }
1115
1264
  if (current === undefined) {
1116
1265
  deps.stdout(ui.style.dim(`No running mono-agent instance for ${target.configPath}.`) + "\n");
@@ -1126,7 +1275,32 @@ export async function statusBackground(target, deps, options = {}) {
1126
1275
  deps.stdout(`${formatOtherInstance(source)}\n`);
1127
1276
  }
1128
1277
  }
1129
- return current?.health === "running" ? 0 : 1;
1278
+ return active ? 0 : 1;
1279
+ }
1280
+ function inactiveStatusSource(source) {
1281
+ const { transports: _staleTransports, pid: _stalePid, ...withoutLiveProcessFacts } = source;
1282
+ const metadata = source.metadata;
1283
+ const channels = metadata?.channels;
1284
+ const stoppedChannels = isPlainRecord(channels)
1285
+ ? Object.fromEntries(Object.entries(channels).map(([id, value]) => {
1286
+ if (!isPlainRecord(value) || value.kind !== "running")
1287
+ return [id, value];
1288
+ return [id, { kind: "stopped", reason: "instance is not running" }];
1289
+ }))
1290
+ : undefined;
1291
+ return {
1292
+ ...withoutLiveProcessFacts,
1293
+ health: "stopped",
1294
+ status: "stopped",
1295
+ ...(metadata === undefined
1296
+ ? {}
1297
+ : {
1298
+ metadata: {
1299
+ ...metadata,
1300
+ ...(stoppedChannels === undefined ? {} : { channels: stoppedChannels }),
1301
+ },
1302
+ }),
1303
+ };
1130
1304
  }
1131
1305
  /**
1132
1306
  * The machine-readable instance record for `status --json`. It mirrors the
@@ -1295,8 +1469,24 @@ function reportTimeout(deps) {
1295
1469
  deps.stderr(ui.errorLine("mono-agent did not report ready within the timeout."));
1296
1470
  deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
1297
1471
  }
1298
- async function cleanUpUnreadyBackground(target, deps, poll) {
1472
+ async function cleanUpUnreadyBackground(target, deps, poll, options = {}) {
1299
1473
  try {
1474
+ if (options.preserveDefinitionsOnFailure === true) {
1475
+ const uid = deps.getuid();
1476
+ const [service, sources] = await Promise.all([
1477
+ launchdServiceInfo(deps.runner, target.label, uid),
1478
+ findInstances(target, deps),
1479
+ ]);
1480
+ const liveTracePids = uniquePids(sources
1481
+ .filter((source) => source.health !== "stopped"
1482
+ && source.pid !== undefined
1483
+ && deps.isAlive(source.pid))
1484
+ .map((source) => source.pid));
1485
+ if (liveTracePids.some((pid) => pid !== service.pid))
1486
+ return false;
1487
+ const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid, ...liveTracePids]), deps, uid, poll);
1488
+ return stopped.ok;
1489
+ }
1300
1490
  return await stopBackgroundUnlocked(target, { ...deps, stdout: () => undefined }, poll) === 0;
1301
1491
  }
1302
1492
  catch (error) {
@@ -1304,9 +1494,11 @@ async function cleanUpUnreadyBackground(target, deps, poll) {
1304
1494
  return false;
1305
1495
  }
1306
1496
  }
1307
- function reportReadinessCleanup(target, deps, stopped) {
1497
+ function reportReadinessCleanup(target, deps, stopped, options = {}) {
1308
1498
  deps.stderr(stopped
1309
- ? ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
1499
+ ? options.preserveDefinitionsOnFailure === true
1500
+ ? ui.style.dim("The unready main LaunchAgent was stopped; both definitions and the scheduled recovery controller remain for retry.") + "\n"
1501
+ : ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
1310
1502
  : ui.style.yellow("The unready LaunchAgent could not be proven stopped; a worker or maintenance helper may still be running.") + "\n");
1311
1503
  reportLaunchRecovery(target, deps);
1312
1504
  }