@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.
- package/README.md +190 -33
- package/dist/adapter-send-tools-main.js +0 -1
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +8 -5
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +195 -257
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +2 -1
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js +1 -5
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +2 -6
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +2 -1
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +66 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-memory-health.d.ts +1 -1
- package/dist/app-controller-memory-health.d.ts.map +1 -1
- package/dist/app-controller-memory-health.js +5 -1
- package/dist/app-controller-memory-health.js.map +1 -1
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +12 -4
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +15 -1
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-utils.js +1 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +14 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +61 -9
- package/dist/app-controller.js.map +1 -1
- package/dist/audit-runs.d.ts.map +1 -1
- package/dist/audit-runs.js +12 -2
- package/dist/audit-runs.js.map +1 -1
- package/dist/background-environment.d.ts +1 -0
- package/dist/background-environment.d.ts.map +1 -1
- package/dist/background-environment.js +1 -0
- package/dist/background-environment.js.map +1 -1
- package/dist/background-runtime.d.ts +39 -2
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +901 -49
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +36 -3
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +404 -57
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.js +8 -1
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/live.d.ts +3 -2
- package/dist/channel-drivers/live.d.ts.map +1 -1
- package/dist/channel-drivers/live.js +3 -2
- package/dist/channel-drivers/live.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +2 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +14 -2
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +55 -0
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +19 -14
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +8 -0
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channel-runtime-controls.d.ts +23 -0
- package/dist/channel-runtime-controls.d.ts.map +1 -0
- package/dist/channel-runtime-controls.js +63 -0
- package/dist/channel-runtime-controls.js.map +1 -0
- package/dist/channel-status-display.d.ts +15 -0
- package/dist/channel-status-display.d.ts.map +1 -0
- package/dist/channel-status-display.js +48 -0
- package/dist/channel-status-display.js.map +1 -0
- package/dist/cli-args.d.ts +23 -22
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +136 -83
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +1 -0
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +158 -27
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts +2 -2
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +27 -59
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts +19 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +203 -67
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-init-command.d.ts.map +1 -1
- package/dist/cli-init-command.js +4 -18
- package/dist/cli-init-command.js.map +1 -1
- package/dist/cli-install-skill-command.d.ts.map +1 -1
- package/dist/cli-install-skill-command.js +32 -3
- package/dist/cli-install-skill-command.js.map +1 -1
- package/dist/cli-runs-command.d.ts +10 -0
- package/dist/cli-runs-command.d.ts.map +1 -0
- package/dist/cli-runs-command.js +66 -0
- package/dist/cli-runs-command.js.map +1 -0
- package/dist/cli-sandbox-command.d.ts +1 -1
- package/dist/cli-sandbox-command.d.ts.map +1 -1
- package/dist/cli-sandbox-command.js +14 -2
- package/dist/cli-sandbox-command.js.map +1 -1
- package/dist/cli-validate-config-command.d.ts +24 -5
- package/dist/cli-validate-config-command.d.ts.map +1 -1
- package/dist/cli-validate-config-command.js +120 -41
- package/dist/cli-validate-config-command.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +4 -5
- package/dist/config-reference.js.map +1 -1
- package/dist/docs-mcp-pairing.d.ts +35 -0
- package/dist/docs-mcp-pairing.d.ts.map +1 -0
- package/dist/docs-mcp-pairing.js +311 -0
- package/dist/docs-mcp-pairing.js.map +1 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -5
- package/dist/doctor.js.map +1 -1
- package/dist/install-skill.d.ts +2 -0
- package/dist/install-skill.d.ts.map +1 -1
- package/dist/install-skill.js +10 -4
- package/dist/install-skill.js.map +1 -1
- package/dist/interaction-bridge.d.ts +5 -20
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +255 -100
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd.d.ts +35 -1
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +131 -2
- package/dist/launchd.js.map +1 -1
- package/dist/managed-runtime-publication.d.ts +22 -0
- package/dist/managed-runtime-publication.d.ts.map +1 -0
- package/dist/managed-runtime-publication.js +91 -0
- package/dist/managed-runtime-publication.js.map +1 -0
- package/dist/managed-startup.d.ts +8 -0
- package/dist/managed-startup.d.ts.map +1 -0
- package/dist/managed-startup.js +12 -0
- package/dist/managed-startup.js.map +1 -0
- package/dist/memory-retrieval.d.ts.map +1 -1
- package/dist/memory-retrieval.js +31 -18
- package/dist/memory-retrieval.js.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +12 -2
- package/dist/metrics.js.map +1 -1
- package/dist/modules/catalog.js +1 -1
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +0 -3
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +7 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/request-model-override.d.ts +2 -2
- package/dist/request-model-override.js +8 -4
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +6 -2
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +511 -118
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-provenance.d.ts.map +1 -1
- package/dist/runtime-provenance.js +10 -5
- package/dist/runtime-provenance.js.map +1 -1
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +2 -1
- package/dist/tui-command.js.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +0 -1
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +1 -8
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +1 -1
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +1 -1
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/presets.d.ts +3 -12
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +3 -15
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +3 -4
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +3 -3
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -16
- package/schema/mono-agent.config.schema.json +4 -4
- package/skills/mono-agent-composer/SKILL.md +13 -5
- package/skills/mono-agent-composer/agents/openai.yaml +1 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +18 -10
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -5
- package/skills/mono-agent-composer/references/feature-coverage.md +10 -9
- package/skills/mono-agent-composer/references/package-map.md +10 -4
- package/skills/mono-agent-composer/references/playbooks.md +6 -6
- package/skills/mono-agent-composer/references/validation.md +24 -2
package/dist/background.js
CHANGED
|
@@ -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 {
|
|
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
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
...
|
|
694
|
+
...readinessPoll,
|
|
523
695
|
sinceMs,
|
|
524
696
|
...(launchTarget.requireTui === true ? { requireTui: true } : {}),
|
|
525
697
|
});
|
|
526
698
|
}
|
|
527
699
|
catch (error) {
|
|
528
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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 =
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
1194
|
-
|
|
1195
|
-
deps.stdout(ui.rule(
|
|
1196
|
-
for (const line of
|
|
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" ||
|
|
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
|
|
1545
|
-
});
|
|
1891
|
+
return { id, kind, text };
|
|
1892
|
+
}));
|
|
1546
1893
|
}
|
|
1547
1894
|
export function describeChannel(value) {
|
|
1548
1895
|
if (value === null || typeof value !== "object") {
|