@mono-agent/agent-app 0.12.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 (200) hide show
  1. package/README.md +190 -33
  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 +195 -257
  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-lifecycle.d.ts.map +1 -1
  15. package/dist/app-controller-lifecycle.js +2 -6
  16. package/dist/app-controller-lifecycle.js.map +1 -1
  17. package/dist/app-controller-maintenance.d.ts +2 -1
  18. package/dist/app-controller-maintenance.d.ts.map +1 -1
  19. package/dist/app-controller-maintenance.js +66 -2
  20. package/dist/app-controller-maintenance.js.map +1 -1
  21. package/dist/app-controller-memory-health.d.ts +1 -1
  22. package/dist/app-controller-memory-health.d.ts.map +1 -1
  23. package/dist/app-controller-memory-health.js +5 -1
  24. package/dist/app-controller-memory-health.js.map +1 -1
  25. package/dist/app-controller-responder.d.ts.map +1 -1
  26. package/dist/app-controller-responder.js +12 -4
  27. package/dist/app-controller-responder.js.map +1 -1
  28. package/dist/app-controller-traceability.d.ts.map +1 -1
  29. package/dist/app-controller-traceability.js +15 -1
  30. package/dist/app-controller-traceability.js.map +1 -1
  31. package/dist/app-controller-utils.js +1 -1
  32. package/dist/app-controller-utils.js.map +1 -1
  33. package/dist/app-controller.d.ts +14 -1
  34. package/dist/app-controller.d.ts.map +1 -1
  35. package/dist/app-controller.js +61 -9
  36. package/dist/app-controller.js.map +1 -1
  37. package/dist/audit-runs.d.ts.map +1 -1
  38. package/dist/audit-runs.js +12 -2
  39. package/dist/audit-runs.js.map +1 -1
  40. package/dist/background-environment.d.ts +1 -0
  41. package/dist/background-environment.d.ts.map +1 -1
  42. package/dist/background-environment.js +1 -0
  43. package/dist/background-environment.js.map +1 -1
  44. package/dist/background-runtime.d.ts +39 -2
  45. package/dist/background-runtime.d.ts.map +1 -1
  46. package/dist/background-runtime.js +901 -49
  47. package/dist/background-runtime.js.map +1 -1
  48. package/dist/background.d.ts +36 -3
  49. package/dist/background.d.ts.map +1 -1
  50. package/dist/background.js +404 -57
  51. package/dist/background.js.map +1 -1
  52. package/dist/channel-drivers/cron.js +8 -1
  53. package/dist/channel-drivers/cron.js.map +1 -1
  54. package/dist/channel-drivers/live.d.ts +3 -2
  55. package/dist/channel-drivers/live.d.ts.map +1 -1
  56. package/dist/channel-drivers/live.js +3 -2
  57. package/dist/channel-drivers/live.js.map +1 -1
  58. package/dist/channel-drivers/native-notify.d.ts +2 -0
  59. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  60. package/dist/channel-drivers/native-notify.js +14 -2
  61. package/dist/channel-drivers/native-notify.js.map +1 -1
  62. package/dist/channel-drivers/slack.d.ts.map +1 -1
  63. package/dist/channel-drivers/slack.js +55 -0
  64. package/dist/channel-drivers/slack.js.map +1 -1
  65. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  66. package/dist/channel-drivers/telegram.js +19 -14
  67. package/dist/channel-drivers/telegram.js.map +1 -1
  68. package/dist/channel-drivers/tui.d.ts.map +1 -1
  69. package/dist/channel-drivers/tui.js +8 -0
  70. package/dist/channel-drivers/tui.js.map +1 -1
  71. package/dist/channel-runtime-controls.d.ts +23 -0
  72. package/dist/channel-runtime-controls.d.ts.map +1 -0
  73. package/dist/channel-runtime-controls.js +63 -0
  74. package/dist/channel-runtime-controls.js.map +1 -0
  75. package/dist/channel-status-display.d.ts +15 -0
  76. package/dist/channel-status-display.d.ts.map +1 -0
  77. package/dist/channel-status-display.js +48 -0
  78. package/dist/channel-status-display.js.map +1 -0
  79. package/dist/cli-args.d.ts +23 -22
  80. package/dist/cli-args.d.ts.map +1 -1
  81. package/dist/cli-args.js +136 -83
  82. package/dist/cli-args.js.map +1 -1
  83. package/dist/cli-background-command.d.ts +1 -0
  84. package/dist/cli-background-command.d.ts.map +1 -1
  85. package/dist/cli-background-command.js +158 -27
  86. package/dist/cli-background-command.js.map +1 -1
  87. package/dist/cli-commands.d.ts +2 -2
  88. package/dist/cli-commands.d.ts.map +1 -1
  89. package/dist/cli-commands.js +27 -59
  90. package/dist/cli-commands.js.map +1 -1
  91. package/dist/cli-help.d.ts +19 -1
  92. package/dist/cli-help.d.ts.map +1 -1
  93. package/dist/cli-help.js +203 -67
  94. package/dist/cli-help.js.map +1 -1
  95. package/dist/cli-init-command.d.ts.map +1 -1
  96. package/dist/cli-init-command.js +4 -18
  97. package/dist/cli-init-command.js.map +1 -1
  98. package/dist/cli-install-skill-command.d.ts.map +1 -1
  99. package/dist/cli-install-skill-command.js +32 -3
  100. package/dist/cli-install-skill-command.js.map +1 -1
  101. package/dist/cli-runs-command.d.ts +10 -0
  102. package/dist/cli-runs-command.d.ts.map +1 -0
  103. package/dist/cli-runs-command.js +66 -0
  104. package/dist/cli-runs-command.js.map +1 -0
  105. package/dist/cli-sandbox-command.d.ts +1 -1
  106. package/dist/cli-sandbox-command.d.ts.map +1 -1
  107. package/dist/cli-sandbox-command.js +14 -2
  108. package/dist/cli-sandbox-command.js.map +1 -1
  109. package/dist/cli-validate-config-command.d.ts +24 -5
  110. package/dist/cli-validate-config-command.d.ts.map +1 -1
  111. package/dist/cli-validate-config-command.js +120 -41
  112. package/dist/cli-validate-config-command.js.map +1 -1
  113. package/dist/config-reference.d.ts.map +1 -1
  114. package/dist/config-reference.js +4 -5
  115. package/dist/config-reference.js.map +1 -1
  116. package/dist/docs-mcp-pairing.d.ts +35 -0
  117. package/dist/docs-mcp-pairing.d.ts.map +1 -0
  118. package/dist/docs-mcp-pairing.js +311 -0
  119. package/dist/docs-mcp-pairing.js.map +1 -0
  120. package/dist/doctor.d.ts +6 -0
  121. package/dist/doctor.d.ts.map +1 -1
  122. package/dist/doctor.js +4 -5
  123. package/dist/doctor.js.map +1 -1
  124. package/dist/install-skill.d.ts +2 -0
  125. package/dist/install-skill.d.ts.map +1 -1
  126. package/dist/install-skill.js +10 -4
  127. package/dist/install-skill.js.map +1 -1
  128. package/dist/interaction-bridge.d.ts +5 -20
  129. package/dist/interaction-bridge.d.ts.map +1 -1
  130. package/dist/interaction-bridge.js +255 -100
  131. package/dist/interaction-bridge.js.map +1 -1
  132. package/dist/launchd.d.ts +35 -1
  133. package/dist/launchd.d.ts.map +1 -1
  134. package/dist/launchd.js +131 -2
  135. package/dist/launchd.js.map +1 -1
  136. package/dist/managed-runtime-publication.d.ts +22 -0
  137. package/dist/managed-runtime-publication.d.ts.map +1 -0
  138. package/dist/managed-runtime-publication.js +91 -0
  139. package/dist/managed-runtime-publication.js.map +1 -0
  140. package/dist/managed-startup.d.ts +8 -0
  141. package/dist/managed-startup.d.ts.map +1 -0
  142. package/dist/managed-startup.js +12 -0
  143. package/dist/managed-startup.js.map +1 -0
  144. package/dist/memory-retrieval.d.ts.map +1 -1
  145. package/dist/memory-retrieval.js +31 -18
  146. package/dist/memory-retrieval.js.map +1 -1
  147. package/dist/metrics.d.ts.map +1 -1
  148. package/dist/metrics.js +12 -2
  149. package/dist/metrics.js.map +1 -1
  150. package/dist/modules/catalog.js +1 -1
  151. package/dist/modules/catalog.js.map +1 -1
  152. package/dist/modules/known-tools.d.ts +1 -1
  153. package/dist/modules/known-tools.d.ts.map +1 -1
  154. package/dist/modules/known-tools.js +0 -3
  155. package/dist/modules/known-tools.js.map +1 -1
  156. package/dist/posted-reply-history.d.ts.map +1 -1
  157. package/dist/posted-reply-history.js +7 -0
  158. package/dist/posted-reply-history.js.map +1 -1
  159. package/dist/request-model-override.d.ts +2 -2
  160. package/dist/request-model-override.js +8 -4
  161. package/dist/request-model-override.js.map +1 -1
  162. package/dist/run-history.d.ts +6 -2
  163. package/dist/run-history.d.ts.map +1 -1
  164. package/dist/run-history.js +511 -118
  165. package/dist/run-history.js.map +1 -1
  166. package/dist/runtime-provenance.d.ts.map +1 -1
  167. package/dist/runtime-provenance.js +10 -5
  168. package/dist/runtime-provenance.js.map +1 -1
  169. package/dist/tui-command.d.ts.map +1 -1
  170. package/dist/tui-command.js +2 -1
  171. package/dist/tui-command.js.map +1 -1
  172. package/dist/ui.js +1 -1
  173. package/dist/ui.js.map +1 -1
  174. package/dist/wizard/from-flags.d.ts +0 -1
  175. package/dist/wizard/from-flags.d.ts.map +1 -1
  176. package/dist/wizard/from-flags.js +1 -8
  177. package/dist/wizard/from-flags.js.map +1 -1
  178. package/dist/wizard/index.d.ts +1 -1
  179. package/dist/wizard/index.d.ts.map +1 -1
  180. package/dist/wizard/index.js +1 -1
  181. package/dist/wizard/index.js.map +1 -1
  182. package/dist/wizard/presets.d.ts +3 -12
  183. package/dist/wizard/presets.d.ts.map +1 -1
  184. package/dist/wizard/presets.js +3 -15
  185. package/dist/wizard/presets.js.map +1 -1
  186. package/dist/wizard/prompts.d.ts.map +1 -1
  187. package/dist/wizard/prompts.js +3 -4
  188. package/dist/wizard/prompts.js.map +1 -1
  189. package/dist/wizard/run.js +3 -3
  190. package/dist/wizard/run.js.map +1 -1
  191. package/package.json +15 -16
  192. package/schema/mono-agent.config.schema.json +4 -4
  193. package/skills/mono-agent-composer/SKILL.md +13 -5
  194. package/skills/mono-agent-composer/agents/openai.yaml +1 -1
  195. package/skills/mono-agent-composer/references/config-blueprint.md +18 -10
  196. package/skills/mono-agent-composer/references/discovery-questions.md +8 -5
  197. package/skills/mono-agent-composer/references/feature-coverage.md +10 -9
  198. package/skills/mono-agent-composer/references/package-map.md +10 -4
  199. package/skills/mono-agent-composer/references/playbooks.md +6 -6
  200. package/skills/mono-agent-composer/references/validation.md +24 -2
@@ -7,12 +7,15 @@ 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, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
10
+ import { formatHumanChannelSections } from "./channel-status-display.js";
11
+ import { hasCompletedManagedStartup } from "./managed-startup.js";
12
+ import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdManagedWorkerInfo, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
11
13
  import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
12
14
  import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, markLaunchdLogMaintenanceStopping, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
13
- import { ensureManagedBackgroundRuntime, MANAGED_BACKGROUND_WORKER_ENV, } from "./background-runtime.js";
15
+ import { ensureManagedBackgroundRuntime, inspectManagedRuntimeSourceIdentity, MANAGED_BACKGROUND_WORKER_ENV, verifyManagedRuntimeLaunch, } from "./background-runtime.js";
14
16
  import { backgroundSnapshotFromMetadata, captureBackgroundSnapshot, encodeBackgroundSnapshot, sameBackgroundSnapshot, } from "./background-snapshot.js";
15
17
  import { resolveConfiguredManagedRuntimePackages } from "./managed-runtime-packages.js";
18
+ import { acquireManagedRuntimePublicationBarrier } from "./managed-runtime-publication.js";
16
19
  import { processIncarnationFromJson, } from "./process-incarnation.js";
17
20
  import { acquireOwnerPrivateLock, validateOwnerPrivateLockInputs } from "./owner-private-lock.js";
18
21
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
@@ -65,6 +68,7 @@ export async function resolveInstanceTarget(input) {
65
68
  paths: launchdPathsFor(label),
66
69
  nodePath: process.execPath,
67
70
  cliPath: input.cliPath,
71
+ controllerCliPath: input.cliPath,
68
72
  // Bake an explicit --env-file (resolved absolute) into the plist so the
69
73
  // launchd worker loads the same env file the launcher did.
70
74
  ...(input.args.envFile === undefined ? {} : { envFile: resolve(cwd, input.args.envFile) }),
@@ -107,6 +111,7 @@ export function defaultBackgroundDeps() {
107
111
  return {
108
112
  runner: makeLaunchctlRunner(),
109
113
  getuid: () => process.getuid?.() ?? 0,
114
+ currentPid: () => process.pid,
110
115
  now: () => Date.now(),
111
116
  sleep: (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
112
117
  listRecordedRuns,
@@ -140,12 +145,18 @@ export function defaultBackgroundDeps() {
140
145
  }
141
146
  },
142
147
  ensureManagedRuntime: (input) => ensureManagedBackgroundRuntime(input),
148
+ inspectManagedRuntimeSourceIdentity,
149
+ verifyManagedRuntimeLaunch,
143
150
  resolveManagedRuntimePackages: (target) => resolveConfiguredManagedRuntimePackages({
144
151
  cwd: target.cwd,
145
152
  configPath: target.configPath,
146
153
  env: target.configurationEnvironment,
147
154
  }),
148
155
  acquireLifecycleLock: acquireFilesystemLifecycleLock,
156
+ acquireRuntimePublicationBarrier: (target) => acquireManagedRuntimePublicationBarrier({
157
+ label: target.label,
158
+ managedRoot: dirname(target.paths.logDir),
159
+ }),
149
160
  probeTui: probeTuiEndpoint,
150
161
  stdout: (text) => void process.stdout.write(text),
151
162
  stderr: (text) => void process.stderr.write(text),
@@ -156,20 +167,133 @@ export function defaultBackgroundDeps() {
156
167
  }),
157
168
  };
158
169
  }
159
- const DEFAULT_POLL = { timeoutMs: 18_000, intervalMs: 400 };
160
- export async function startBackground(target, deps, poll = DEFAULT_POLL) {
170
+ const DEFAULT_CONTROL_POLL = { timeoutMs: 18_000, intervalMs: 400 };
171
+ const DEFAULT_READINESS_POLL = { timeoutMs: 60_000, intervalMs: 400 };
172
+ export async function startBackground(target, deps, poll) {
161
173
  return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
162
174
  }
163
175
  /** Restart is behaviourally identical: ensure a single fresh running instance. */
164
- export async function restartBackground(target, deps, poll = DEFAULT_POLL) {
176
+ export async function restartBackground(target, deps, poll) {
165
177
  return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
166
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
+ }
167
291
  /**
168
292
  * Stop, perform the caller's stopped-worker mutation, and start again while
169
293
  * retaining one lifecycle lock. This closes the force-restart gap in which a
170
294
  * concurrent start could previously enter while the session store was purged.
171
295
  */
172
- export async function forceRestartBackground(target, deps, whileStopped, poll = DEFAULT_POLL) {
296
+ export async function forceRestartBackground(target, deps, whileStopped, poll) {
173
297
  const release = await deps.acquireLifecycleLock(target);
174
298
  if (release === undefined) {
175
299
  deps.stderr(ui.errorLine(`Another lifecycle command is already active for ${target.label}.`));
@@ -177,11 +301,12 @@ export async function forceRestartBackground(target, deps, whileStopped, poll =
177
301
  return 1;
178
302
  }
179
303
  try {
180
- const stopCode = await stopBackgroundUnlocked(target, deps, poll);
304
+ const controlPoll = poll ?? DEFAULT_CONTROL_POLL;
305
+ const stopCode = await stopBackgroundUnlocked(target, deps, controlPoll);
181
306
  if (stopCode !== 0)
182
307
  return stopCode;
183
308
  await whileStopped();
184
- return (await ensureBackgroundReadyUnlocked(target, deps, poll)).ok ? 0 : 1;
309
+ return (await ensureBackgroundReadyUnlocked(target, deps, controlPoll, poll ?? DEFAULT_READINESS_POLL)).ok ? 0 : 1;
185
310
  }
186
311
  finally {
187
312
  await release().catch((error) => {
@@ -195,7 +320,7 @@ export async function forceRestartBackground(target, deps, whileStopped, poll =
195
320
  * intentionally stopped service; the sole exception is recovery authorized by
196
321
  * its own durable lifecycle intent after a prior maintainer died post-bootout.
197
322
  */
198
- export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_POLL) {
323
+ export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_CONTROL_POLL) {
199
324
  const release = await deps.acquireLifecycleLock(target);
200
325
  if (release === undefined) {
201
326
  // Controller start/restart/stop owns the same lock. Skipping this periodic
@@ -398,11 +523,11 @@ function maintenanceErrorMessage(error) {
398
523
  }
399
524
  /**
400
525
  * Ensure the canonical per-config LaunchAgent is running and return the fresh
401
- * trace source that proved it reached `startup-complete`. This is the shared
526
+ * trace source that proved it completed the full startup lifecycle. This is the shared
402
527
  * lifecycle boundary for CLI start/restart and remote configuration handoffs;
403
528
  * callers must not open a console when the result is not ok.
404
529
  */
405
- export async function ensureBackgroundReady(target, deps, poll = DEFAULT_POLL) {
530
+ export async function ensureBackgroundReady(target, deps, poll) {
406
531
  const release = await deps.acquireLifecycleLock(target);
407
532
  if (release === undefined) {
408
533
  deps.stderr(ui.errorLine(`Another lifecycle command is already active for ${target.label}.`));
@@ -410,7 +535,7 @@ export async function ensureBackgroundReady(target, deps, poll = DEFAULT_POLL) {
410
535
  return { ok: false, action: "start", reason: "ownership" };
411
536
  }
412
537
  try {
413
- return await ensureBackgroundReadyUnlocked(target, deps, poll);
538
+ return await ensureBackgroundReadyUnlocked(target, deps, poll ?? DEFAULT_CONTROL_POLL, poll ?? DEFAULT_READINESS_POLL);
414
539
  }
415
540
  finally {
416
541
  await release().catch((error) => {
@@ -418,18 +543,58 @@ export async function ensureBackgroundReady(target, deps, poll = DEFAULT_POLL) {
418
543
  });
419
544
  }
420
545
  }
421
- async function ensureBackgroundReadyUnlocked(target, deps, poll) {
546
+ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll, options = {}) {
422
547
  if (target.expectedSnapshot === undefined) {
423
548
  deps.stderr(ui.errorLine("Refusing to launch a managed worker without an approved background snapshot."));
424
549
  deps.stderr(ui.hint("No LaunchAgent changes were made. Retry from the wizard or `mono-agent start`."));
425
550
  return { ok: false, action: "start", reason: "snapshot" };
426
551
  }
427
- const uid = deps.getuid();
428
552
  if (!(await snapshotStillMatches(target, deps))) {
429
553
  reportSnapshotDrift(target, deps);
430
554
  return { ok: false, action: "start", reason: "snapshot" };
431
555
  }
556
+ let publicationBarrier;
557
+ try {
558
+ publicationBarrier = await (deps.acquireRuntimePublicationBarrier
559
+ ?? ((input) => acquireManagedRuntimePublicationBarrier({
560
+ label: input.label,
561
+ managedRoot: dirname(input.paths.logDir),
562
+ })))(target);
563
+ }
564
+ catch (error) {
565
+ reportLifecycleException(target, deps, "establish the managed runtime publication barrier", error);
566
+ return { ok: false, action: "start", reason: "ownership" };
567
+ }
568
+ if (publicationBarrier === undefined) {
569
+ deps.stderr(ui.errorLine(`Another runtime publication is already active for ${target.label}.`));
570
+ return { ok: false, action: "start", reason: "ownership" };
571
+ }
572
+ return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options);
573
+ }
574
+ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options) {
575
+ let barrierReleased = false;
576
+ const releaseBarrier = async () => {
577
+ if (barrierReleased)
578
+ return;
579
+ await publicationBarrier.release();
580
+ barrierReleased = true;
581
+ };
582
+ try {
583
+ return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options);
584
+ }
585
+ finally {
586
+ if (!barrierReleased) {
587
+ await releaseBarrier().catch((error) => {
588
+ deps.stderr(ui.errorLine(`Could not release runtime publication barrier for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
589
+ });
590
+ }
591
+ }
592
+ }
593
+ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options) {
594
+ const uid = deps.getuid();
432
595
  let launchTarget;
596
+ const runtimeStartedAt = deps.now();
597
+ deps.stdout(ui.hint("Verifying the durable managed runtime…"));
433
598
  try {
434
599
  const additionalPackages = await deps.resolveManagedRuntimePackages?.(target) ?? [];
435
600
  const runtime = await deps.ensureManagedRuntime({
@@ -441,7 +606,10 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
441
606
  ...target,
442
607
  cliPath: runtime.cliPath,
443
608
  nodePath: runtime.nodePath,
609
+ managedRuntimeLaunchProof: runtime.launchProof,
444
610
  };
611
+ deps.stdout(ui.hint(`Managed runtime ready (${runtimeVerificationLabel(runtime.verificationMode)}, `
612
+ + `${formatLifecycleDuration(deps.now() - runtimeStartedAt)}).`));
445
613
  }
446
614
  catch (error) {
447
615
  reportLifecycleException(target, deps, "install and verify the durable managed runtime", error);
@@ -475,6 +643,7 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
475
643
  }
476
644
  let outcome;
477
645
  let prepared = false;
646
+ deps.stdout(ui.hint("Replacing the managed worker…"));
478
647
  try {
479
648
  let interruptedMaintenance = await deps.readLaunchdLogMaintenanceIntent(launchTarget.paths);
480
649
  if (interruptedMaintenance?.phase === "stopping" || interruptedMaintenance?.phase === "restoring") {
@@ -483,7 +652,7 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
483
652
  throw new Error("Interrupted launchd-log maintenance lost its writer PID before durable stopped-writer proof; refusing restart.");
484
653
  }
485
654
  }
486
- outcome = await bootstrapOrRestart(launchTarget, deps, uid, poll, async () => {
655
+ outcome = await bootstrapOrRestart(launchTarget, deps, uid, controlPoll, async () => {
487
656
  const stoppedIntent = interruptedMaintenance;
488
657
  if (stoppedIntent?.phase === "stopped") {
489
658
  interruptedMaintenance = await deps.markLaunchdLogMaintenanceStopping(launchTarget.paths, stoppedIntent);
@@ -504,7 +673,10 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
504
673
  await writePlists(launchTarget, deps);
505
674
  sinceMs = deps.now();
506
675
  prepared = true;
507
- });
676
+ // The replacement plist now contains the finalized-runtime proof. Let
677
+ // launchd respawn only after the stopped-window commit is complete.
678
+ await releaseBarrier();
679
+ }, { preserveMaintenanceService: options.preserveMaintenanceService === true });
508
680
  }
509
681
  catch (error) {
510
682
  reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
@@ -519,18 +691,20 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
519
691
  let ready;
520
692
  try {
521
693
  ready = await pollInstanceReady(launchTarget, deps, {
522
- ...poll,
694
+ ...readinessPoll,
523
695
  sinceMs,
524
696
  ...(launchTarget.requireTui === true ? { requireTui: true } : {}),
525
697
  });
526
698
  }
527
699
  catch (error) {
528
- reportLifecycleException(launchTarget, deps, "read the worker readiness trace", error);
700
+ reportReadinessException(deps, error);
701
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
702
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
529
703
  return { ok: false, action, reason: "readiness" };
530
704
  }
531
705
  if (ready === undefined) {
532
706
  if (!(await snapshotStillMatches(launchTarget, deps))) {
533
- const stopped = await stopBackgroundUnlocked(launchTarget, deps, poll);
707
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options) ? 0 : 1;
534
708
  reportSnapshotDrift(launchTarget, deps);
535
709
  if (stopped === 0) {
536
710
  deps.stderr(ui.style.dim("The drifted LaunchAgent was stopped before returning control.") + "\n");
@@ -540,13 +714,27 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
540
714
  }
541
715
  return { ok: false, action, reason: "snapshot" };
542
716
  }
543
- reportTimeout(launchTarget, deps);
717
+ reportTimeout(deps);
718
+ const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
719
+ reportReadinessCleanup(launchTarget, deps, stopped, options);
544
720
  return { ok: false, action, reason: "timeout" };
545
721
  }
546
722
  const completedAction = outcome.restarted ? "restarted" : "started";
547
723
  printInstanceInfo(ready, launchTarget, deps, completedAction);
548
724
  return { ok: true, action: completedAction, source: ready };
549
725
  }
726
+ function runtimeVerificationLabel(mode) {
727
+ switch (mode) {
728
+ case "fast-reuse": return "warm reuse";
729
+ case "full-reuse": return "full verification";
730
+ case "installed": return "installed";
731
+ case "repaired": return "repaired";
732
+ }
733
+ }
734
+ function formatLifecycleDuration(milliseconds) {
735
+ const safe = Math.max(0, milliseconds);
736
+ return safe < 1_000 ? `${Math.round(safe)} ms` : `${(safe / 1_000).toFixed(1)} s`;
737
+ }
550
738
  async function prepareLaunchdDirectories(target, deps) {
551
739
  // launchd will not create the log file's parent directory, so make both dirs
552
740
  // before writing the plist that references them.
@@ -558,6 +746,13 @@ async function writePlists(target, deps) {
558
746
  if (target.expectedSnapshot === undefined) {
559
747
  throw new Error("A managed LaunchAgent requires an approved background snapshot.");
560
748
  }
749
+ if (target.managedRuntimeLaunchProof === undefined) {
750
+ throw new Error("A managed LaunchAgent requires a finalized runtime launch proof.");
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
+ }
561
756
  const maintenance = maintenancePathsForTarget(target);
562
757
  const mainXml = buildPlistXml({
563
758
  label: target.label,
@@ -567,6 +762,7 @@ async function writePlists(target, deps) {
567
762
  cwd: target.cwd,
568
763
  ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
569
764
  expectedBackgroundSnapshot: encodeBackgroundSnapshot(target.expectedSnapshot),
765
+ expectedManagedRuntimeLaunch: target.managedRuntimeLaunchProof,
570
766
  stdoutPath: target.paths.stdoutPath,
571
767
  stderrPath: target.paths.stderrPath,
572
768
  environment: target.environment,
@@ -576,6 +772,10 @@ async function writePlists(target, deps) {
576
772
  nodePath: target.nodePath,
577
773
  cliPath: target.cliPath,
578
774
  configPath: target.configPath,
775
+ controllerCliPath: target.controllerCliPath ?? target.cliPath,
776
+ agentCwd: target.cwd,
777
+ agentPath,
778
+ ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
579
779
  // The agent folder may be renamed or removed while its already-running
580
780
  // worker still owns log descriptors. Keep the scheduler's cwd in the
581
781
  // durable account state tree; its config argument is already absolute.
@@ -756,7 +956,7 @@ function sameFileSnapshot(left, right) {
756
956
  * before bootstrap so launchd cannot retain stale ProgramArguments or env from
757
957
  * the previous plist.
758
958
  */
759
- async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
959
+ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped, options = {}) {
760
960
  const maintenance = maintenancePathsForTarget(target);
761
961
  const [service, maintenanceService] = await Promise.all([
762
962
  launchdServiceInfo(deps.runner, target.label, uid),
@@ -766,9 +966,11 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
766
966
  // Stop the scheduler first. A helper that is already running either holds
767
967
  // this lifecycle lock (so the controller could not enter) or is waiting to
768
968
  // 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 };
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
+ }
772
974
  }
773
975
  let mainStopProven = false;
774
976
  if (service.loaded) {
@@ -784,7 +986,9 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
784
986
  ]);
785
987
  const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
786
988
  if (!mainStopped.ok) {
787
- 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);
788
992
  return {
789
993
  ok: false,
790
994
  restarted: true,
@@ -796,19 +1000,28 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
796
1000
  mainStopProven = true;
797
1001
  }
798
1002
  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) {
802
- return {
803
- ok: false,
804
- restarted,
805
- failure: lifecycleFailure(maintenanceBooted, "scheduled log maintenance did not report loaded"),
806
- };
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
+ }
807
1013
  }
808
1014
  const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
809
1015
  if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
810
1016
  return { ok: true, restarted };
811
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
+ }
812
1025
  const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
813
1026
  const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
814
1027
  return {
@@ -913,7 +1126,7 @@ function lifecycleFailure(result, detail) {
913
1126
  function uniquePids(values) {
914
1127
  return [...new Set(values.filter((value) => value !== undefined && value > 0))];
915
1128
  }
916
- export async function stopBackground(target, deps, poll = DEFAULT_POLL) {
1129
+ export async function stopBackground(target, deps, poll = DEFAULT_CONTROL_POLL) {
917
1130
  const release = await deps.acquireLifecycleLock(target);
918
1131
  if (release === undefined) {
919
1132
  deps.stderr(ui.errorLine(`Another lifecycle command is already active for ${target.label}.`));
@@ -1015,13 +1228,39 @@ async function stopBackgroundUnlocked(target, deps, poll) {
1015
1228
  deps.stdout(ui.keyValue([["config", target.configPath]]));
1016
1229
  return 0;
1017
1230
  }
1018
- export async function statusBackground(target, deps) {
1019
- const result = await deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs });
1231
+ export async function statusBackground(target, deps, options = {}) {
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
+ ]);
1020
1236
  const classified = await Promise.all(result.sources.map(async (source) => ({
1021
1237
  source,
1022
1238
  matches: await matchesConfig(source, target.configPath),
1023
1239
  })));
1024
- 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);
1250
+ // Only surface other instances that are live or crashed — cleanly stopped
1251
+ // manifests linger in the registry and would just be noise.
1252
+ const others = classified
1253
+ .filter((entry) => !entry.matches && entry.source.health !== "stopped")
1254
+ .map((entry) => entry.source);
1255
+ if (options.json === true) {
1256
+ const instance = current === undefined ? null : await assembleInstanceStatus(current, target, deps);
1257
+ deps.stdout(`${JSON.stringify({
1258
+ ok: active,
1259
+ instance,
1260
+ others: others.map(assembleOtherInstanceStatus),
1261
+ })}\n`);
1262
+ return active ? 0 : 1;
1263
+ }
1025
1264
  if (current === undefined) {
1026
1265
  deps.stdout(ui.style.dim(`No running mono-agent instance for ${target.configPath}.`) + "\n");
1027
1266
  deps.stdout(ui.hint(`Start it with: mono-agent start${commandFlags(target)}`));
@@ -1030,18 +1269,94 @@ export async function statusBackground(target, deps) {
1030
1269
  writeInstanceDetail(current, target, deps);
1031
1270
  await writeRunsHealthDetail(current, deps);
1032
1271
  }
1033
- // Only surface other instances that are live or crashed — cleanly stopped
1034
- // manifests linger in the registry and would just be noise.
1035
- const others = classified
1036
- .filter((entry) => !entry.matches && entry.source.health !== "stopped")
1037
- .map((entry) => entry.source);
1038
1272
  if (others.length > 0) {
1039
1273
  deps.stdout("\n" + ui.rule("Other mono-agent instances"));
1040
1274
  for (const source of others) {
1041
1275
  deps.stdout(`${formatOtherInstance(source)}\n`);
1042
1276
  }
1043
1277
  }
1044
- 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
+ };
1304
+ }
1305
+ /**
1306
+ * The machine-readable instance record for `status --json`. It mirrors the
1307
+ * fields the human `writeInstanceDetail`/`writeRunsHealthDetail` render, reading
1308
+ * only the already-computed trace-source manifest (content-free, safe to publish)
1309
+ * and the same runs-health probe the human path performs — no extra probes.
1310
+ */
1311
+ async function assembleInstanceStatus(source, target, deps) {
1312
+ const metadata = source.metadata ?? {};
1313
+ const observability = metadata.observability;
1314
+ const sandbox = metadata.sandbox;
1315
+ const session = metadata.session;
1316
+ const channels = metadata.channels;
1317
+ return {
1318
+ sourceId: source.sourceId,
1319
+ label: source.label,
1320
+ launchdLabel: target.label,
1321
+ pid: source.pid ?? null,
1322
+ health: source.health,
1323
+ status: source.status,
1324
+ configPath: target.configPath,
1325
+ startedAt: source.startedAt,
1326
+ updatedAt: source.updatedAt,
1327
+ ...(source.artifactDir === undefined ? {} : { artifactDir: source.artifactDir }),
1328
+ ...(source.transports === undefined ? {} : { transports: source.transports }),
1329
+ logs: { stdout: target.paths.stdoutPath, stderr: target.paths.stderrPath },
1330
+ ...(isPlainRecord(observability) ? { observability } : {}),
1331
+ ...(isPlainRecord(sandbox) ? { sandbox } : {}),
1332
+ ...(isPlainRecord(session) ? { session } : {}),
1333
+ ...(isPlainRecord(channels) ? { channels } : {}),
1334
+ ...(source.memoryHealth === undefined ? {} : { memoryHealth: source.memoryHealth }),
1335
+ runsHealth: await assembleRunsHealthStatus(source, deps),
1336
+ };
1337
+ }
1338
+ async function assembleRunsHealthStatus(source, deps) {
1339
+ if (source.artifactDir === undefined || source.artifactDir.trim().length === 0) {
1340
+ return null;
1341
+ }
1342
+ const { totalRuns, warnings } = await deps.listRecordedRuns({
1343
+ artifactDir: source.artifactDir,
1344
+ maxRuns: RUNS_HEALTH_MAX_RUNS,
1345
+ scope: "agent",
1346
+ });
1347
+ return { totalRuns, warnings };
1348
+ }
1349
+ function assembleOtherInstanceStatus(source) {
1350
+ return {
1351
+ sourceId: source.sourceId,
1352
+ label: source.label,
1353
+ health: source.health,
1354
+ pid: source.pid ?? null,
1355
+ ...(source.configPath === undefined ? {} : { configPath: source.configPath }),
1356
+ };
1357
+ }
1358
+ function isPlainRecord(value) {
1359
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1045
1360
  }
1046
1361
  export async function tailLogs(target, deps, options) {
1047
1362
  const args = [
@@ -1146,9 +1461,45 @@ function reportLifecycleException(target, deps, operation, error) {
1146
1461
  deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
1147
1462
  reportLaunchRecovery(target, deps);
1148
1463
  }
1149
- function reportTimeout(target, deps) {
1464
+ function reportReadinessException(deps, error) {
1465
+ deps.stderr(ui.errorLine(`Failed to read the worker readiness trace: ${error instanceof Error ? error.message : String(error)}`));
1466
+ deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
1467
+ }
1468
+ function reportTimeout(deps) {
1150
1469
  deps.stderr(ui.errorLine("mono-agent did not report ready within the timeout."));
1151
- deps.stderr(ui.style.dim("The committed agent files were preserved. It may still be starting, or it may have failed.") + "\n");
1470
+ deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
1471
+ }
1472
+ async function cleanUpUnreadyBackground(target, deps, poll, options = {}) {
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
+ }
1490
+ return await stopBackgroundUnlocked(target, { ...deps, stdout: () => undefined }, poll) === 0;
1491
+ }
1492
+ catch (error) {
1493
+ deps.stderr(ui.errorLine(`Failed while trying to stop the unready LaunchAgent: ${error instanceof Error ? error.message : String(error)}`));
1494
+ return false;
1495
+ }
1496
+ }
1497
+ function reportReadinessCleanup(target, deps, stopped, options = {}) {
1498
+ deps.stderr(stopped
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"
1502
+ : ui.style.yellow("The unready LaunchAgent could not be proven stopped; a worker or maintenance helper may still be running.") + "\n");
1152
1503
  reportLaunchRecovery(target, deps);
1153
1504
  }
1154
1505
  function reportLaunchRecovery(target, deps) {
@@ -1190,10 +1541,10 @@ function writeInstanceDetail(source, target, deps) {
1190
1541
  deps.stdout(` ${line}\n`);
1191
1542
  }
1192
1543
  }
1193
- const channelLines = formatChannels(source);
1194
- if (channelLines.length > 0) {
1195
- deps.stdout(ui.rule("channels"));
1196
- for (const line of channelLines) {
1544
+ const channelSections = formatChannels(source);
1545
+ for (const section of channelSections) {
1546
+ deps.stdout(ui.rule(section.title));
1547
+ for (const line of section.lines) {
1197
1548
  deps.stdout(`${line}\n`);
1198
1549
  }
1199
1550
  }
@@ -1247,7 +1598,7 @@ async function matchesConfig(source, configPath) {
1247
1598
  return await canonicalBackgroundConfigPath(process.cwd(), source.configPath) === configPath;
1248
1599
  }
1249
1600
  function isReady(source, requireTui) {
1250
- if (source.health !== "running" || metadataReason(source) !== "startup-complete")
1601
+ if (source.health !== "running" || !hasCompletedManagedStartup(source))
1251
1602
  return false;
1252
1603
  if (source.memoryHealth?.status === "unhealthy")
1253
1604
  return false;
@@ -1268,10 +1619,6 @@ function channelRecords(source) {
1268
1619
  return Object.values(channels)
1269
1620
  .filter((value) => value !== null && typeof value === "object");
1270
1621
  }
1271
- function metadataReason(source) {
1272
- const reason = source.metadata?.reason;
1273
- return typeof reason === "string" ? reason : undefined;
1274
- }
1275
1622
  function startedAtMs(source) {
1276
1623
  const parsed = Date.parse(source.startedAt);
1277
1624
  return Number.isFinite(parsed) ? parsed : 0;
@@ -1539,10 +1886,10 @@ function formatChannels(source) {
1539
1886
  if (channels === null || typeof channels !== "object") {
1540
1887
  return [];
1541
1888
  }
1542
- return Object.entries(channels).map(([id, value]) => {
1889
+ return formatHumanChannelSections(Object.entries(channels).map(([id, value]) => {
1543
1890
  const { kind, text } = describeChannel(value);
1544
- return ` ${ui.channelBadge(kind)}${ui.style.bold(id.padEnd(11))} ${text}`;
1545
- });
1891
+ return { id, kind, text };
1892
+ }));
1546
1893
  }
1547
1894
  export function describeChannel(value) {
1548
1895
  if (value === null || typeof value !== "object") {