@jiayunxie/aerial 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/cli.js +22 -0
- package/src/copilot.js +87 -16
- package/src/service.js +118 -13
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -314,6 +314,17 @@ async function main() {
|
|
|
314
314
|
if (r.bootstrap?.stderr) console.log(` bootstrap stderr: ${r.bootstrap.stderr.trim()}`);
|
|
315
315
|
if (r.create?.stderr) console.log(` schtasks stderr: ${r.create.stderr.trim()}`);
|
|
316
316
|
if (r.run?.stderr) console.log(` schtasks /Run stderr: ${r.run.stderr.trim()}`);
|
|
317
|
+
if (r.diagnostics) {
|
|
318
|
+
if (r.diagnostics.stdioLog) console.log(` stdio log: ${r.diagnostics.stdioLog}`);
|
|
319
|
+
if (r.diagnostics.aerialLog) console.log(` aerial log: ${r.diagnostics.aerialLog}`);
|
|
320
|
+
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
321
|
+
if (r.diagnostics.health) {
|
|
322
|
+
const h = r.diagnostics.health;
|
|
323
|
+
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
324
|
+
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
325
|
+
}
|
|
326
|
+
console.log(` Run: aerial service status --json`);
|
|
327
|
+
}
|
|
317
328
|
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
318
329
|
process.exitCode = r.ok ? 0 : 1;
|
|
319
330
|
} catch (err) {
|
|
@@ -329,6 +340,17 @@ async function main() {
|
|
|
329
340
|
else if (r.ok) console.log(`Service start: ok (${r.platform})`);
|
|
330
341
|
else console.log(`Service start: FAILED (${r.reason || `status=${r.status}`})${r.message ? ": " + r.message : ""}`);
|
|
331
342
|
if (r.stderr) console.log(` ${r.stderr.trim()}`);
|
|
343
|
+
if (r.diagnostics) {
|
|
344
|
+
if (r.diagnostics.stdioLog) console.log(` stdio log: ${r.diagnostics.stdioLog}`);
|
|
345
|
+
if (r.diagnostics.aerialLog) console.log(` aerial log: ${r.diagnostics.aerialLog}`);
|
|
346
|
+
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
347
|
+
if (r.diagnostics.health) {
|
|
348
|
+
const h = r.diagnostics.health;
|
|
349
|
+
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
350
|
+
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
351
|
+
}
|
|
352
|
+
console.log(` Run: aerial service status --json`);
|
|
353
|
+
}
|
|
332
354
|
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
333
355
|
process.exitCode = r.ok ? 0 : 1;
|
|
334
356
|
} catch (err) {
|
package/src/copilot.js
CHANGED
|
@@ -174,28 +174,99 @@ function withDefaultAnthropicCache(payload) {
|
|
|
174
174
|
return next;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function
|
|
177
|
+
function canonicalClaudeOpus47Family(model) {
|
|
178
|
+
return /^claude-opus-4[.-]7(?:-|$)/.test(model) ? "claude-opus-4.7" : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function modelHasMessagesRoute(model) {
|
|
182
|
+
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
183
|
+
const routes = Array.isArray(model?.aerial?.routes) ? model.aerial.routes : [];
|
|
184
|
+
return endpoints.includes("/v1/messages") || routes.includes("messages");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function supportedReasoningEfforts(model) {
|
|
188
|
+
const supports = model?.capabilities?.supports;
|
|
189
|
+
const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
|
|
190
|
+
if (Array.isArray(values)) return values.map(String);
|
|
191
|
+
if (typeof values === "string") return [values];
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function modelSupportsAdaptiveThinking(model) {
|
|
196
|
+
const supports = model?.capabilities?.supports;
|
|
197
|
+
return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findAnthropicEffortModel(modelId, effort, models) {
|
|
201
|
+
if (!Array.isArray(models)) return undefined;
|
|
202
|
+
const family = canonicalClaudeOpus47Family(modelId);
|
|
203
|
+
if (!family) return undefined;
|
|
204
|
+
const candidates = models.filter((model) => {
|
|
205
|
+
const id = typeof model?.id === "string" ? model.id : "";
|
|
206
|
+
return canonicalClaudeOpus47Family(id) === family &&
|
|
207
|
+
modelHasMessagesRoute(model) &&
|
|
208
|
+
modelSupportsAdaptiveThinking(model) &&
|
|
209
|
+
supportedReasoningEfforts(model).includes(effort);
|
|
210
|
+
});
|
|
211
|
+
return candidates.find((model) => model.id === modelId) || candidates[0];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function withSupportedAnthropicEffort(payload, models) {
|
|
178
215
|
const effort = payload?.output_config?.effort;
|
|
179
216
|
if (effort === undefined) return payload;
|
|
180
217
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
181
|
-
if (
|
|
182
|
-
const routes = {
|
|
183
|
-
low: "claude-opus-4.7-1m-internal",
|
|
184
|
-
medium: "claude-opus-4.7",
|
|
185
|
-
high: "claude-opus-4.7-high",
|
|
186
|
-
xhigh: "claude-opus-4.7-xhigh",
|
|
187
|
-
max: "claude-opus-4.7-xhigh"
|
|
188
|
-
};
|
|
189
|
-
const nextModel = routes[effort];
|
|
218
|
+
if (!canonicalClaudeOpus47Family(model)) return payload;
|
|
190
219
|
const nextEffort = effort === "max" ? "xhigh" : effort;
|
|
191
|
-
if (!
|
|
220
|
+
if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
|
|
221
|
+
const routed = findAnthropicEffortModel(model, nextEffort, models);
|
|
222
|
+
const nextModel = routed?.id || model;
|
|
192
223
|
if (model === nextModel && effort === nextEffort) return payload;
|
|
193
224
|
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort: nextEffort });
|
|
194
225
|
return { ...payload, model: nextModel, output_config: { ...payload.output_config, effort: nextEffort } };
|
|
195
226
|
}
|
|
196
227
|
|
|
197
|
-
function
|
|
198
|
-
|
|
228
|
+
function legacyThinkingEffort(thinking) {
|
|
229
|
+
if (typeof thinking?.effort === "string" && thinking.effort.trim()) return thinking.effort.trim();
|
|
230
|
+
const budget = Number(thinking?.budget_tokens);
|
|
231
|
+
if (!Number.isFinite(budget) || budget <= 0) return "medium";
|
|
232
|
+
if (budget <= 4096) return "low";
|
|
233
|
+
if (budget <= 16000) return "medium";
|
|
234
|
+
if (budget <= 64000) return "high";
|
|
235
|
+
return "xhigh";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isLegacyThinkingEnabled(thinking) {
|
|
239
|
+
if (!thinking || typeof thinking !== "object" || Array.isArray(thinking)) return false;
|
|
240
|
+
if (thinking.type === "enabled") return true;
|
|
241
|
+
return Boolean(thinking.type && typeof thinking.type === "object" && thinking.type.enabled);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function withSupportedAnthropicThinking(payload) {
|
|
245
|
+
if (!isLegacyThinkingEnabled(payload?.thinking)) return payload;
|
|
246
|
+
const outputConfig = payload?.output_config && typeof payload.output_config === "object" && !Array.isArray(payload.output_config)
|
|
247
|
+
? payload.output_config
|
|
248
|
+
: {};
|
|
249
|
+
const effort = outputConfig.effort ?? legacyThinkingEffort(payload.thinking);
|
|
250
|
+
logEvent("anthropic_thinking_route", { model: payload.model, routedType: "adaptive", routedEffort: effort });
|
|
251
|
+
return {
|
|
252
|
+
...payload,
|
|
253
|
+
thinking: { type: "adaptive" },
|
|
254
|
+
output_config: { ...outputConfig, effort }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function shouldLoadAnthropicCatalog(payload) {
|
|
259
|
+
const effort = payload?.output_config?.effort;
|
|
260
|
+
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
261
|
+
return effort !== undefined && Boolean(canonicalClaudeOpus47Family(model));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function withAnthropicDefaults(payload) {
|
|
265
|
+
const next = withSupportedAnthropicThinking(withDefaultAnthropicCache(payload));
|
|
266
|
+
const models = shouldLoadAnthropicCatalog(next)
|
|
267
|
+
? await fetchModelsCatalog().catch(() => undefined)
|
|
268
|
+
: undefined;
|
|
269
|
+
return withSupportedAnthropicEffort(next, models);
|
|
199
270
|
}
|
|
200
271
|
|
|
201
272
|
function parseJsonBody(body, contentType) {
|
|
@@ -380,7 +451,7 @@ function wrapResponseWithCacheObserver(upstream, route, requestCache) {
|
|
|
380
451
|
|
|
381
452
|
async function requestWithJsonBody(request, transform) {
|
|
382
453
|
const payload = await request.json();
|
|
383
|
-
const nextPayload = transform(payload);
|
|
454
|
+
const nextPayload = await transform(payload);
|
|
384
455
|
return new Request(request.url, {
|
|
385
456
|
method: request.method,
|
|
386
457
|
headers: request.headers,
|
|
@@ -408,7 +479,7 @@ async function proxyFetch(path, request, { extraHeaders = {}, bodyOverride } = {
|
|
|
408
479
|
return wrapResponseWithCacheObserver(upstream, path, requestCache);
|
|
409
480
|
}
|
|
410
481
|
|
|
411
|
-
async function
|
|
482
|
+
async function fetchModelsCatalog() {
|
|
412
483
|
const token = await getCopilotToken();
|
|
413
484
|
const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
|
|
414
485
|
if (!response.ok) return undefined;
|
|
@@ -434,7 +505,7 @@ export async function proxyResponses(request) {
|
|
|
434
505
|
// per-request /models lookup to avoid an extra upstream call and let the
|
|
435
506
|
// shared HTTP path handle cache_request logging via proxyFetch.
|
|
436
507
|
if (payload?.stream && isResponsesWebSocketOptIn()) {
|
|
437
|
-
const models = await
|
|
508
|
+
const models = await fetchModelsCatalog().catch(() => undefined);
|
|
438
509
|
const model = models ? responseModelFromCatalog(payload.model, models) : undefined;
|
|
439
510
|
if (shouldUseResponsesWebSocket(payload, model)) {
|
|
440
511
|
const requestCache = cacheRequestFields(payload);
|
package/src/service.js
CHANGED
|
@@ -15,6 +15,7 @@ const DEFAULT_WRAPPER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
15
15
|
const DEFAULT_WRAPPER_LOG_BACKUPS = 3;
|
|
16
16
|
const HEALTH_TIMEOUT_MS = 1500;
|
|
17
17
|
const HEALTH_START_TIMEOUT_MS = 5000;
|
|
18
|
+
const HEALTH_POLL_INTERVAL_MS = 250;
|
|
18
19
|
|
|
19
20
|
function wrapperLogConfig() {
|
|
20
21
|
const out = { maxBytes: DEFAULT_WRAPPER_LOG_MAX_BYTES, backups: DEFAULT_WRAPPER_LOG_BACKUPS };
|
|
@@ -339,6 +340,53 @@ function classifyHealth(probe) {
|
|
|
339
340
|
return { mode: "aerial_running", body: probe.body };
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
async function pollForAerialUp(host, port, healthFetch, deadlineMs = HEALTH_START_TIMEOUT_MS) {
|
|
344
|
+
const fetcher = healthFetch || defaultHealthFetch;
|
|
345
|
+
const start = Date.now();
|
|
346
|
+
let lastProbe;
|
|
347
|
+
let lastCls;
|
|
348
|
+
let attempts = 0;
|
|
349
|
+
while (Date.now() - start < deadlineMs) {
|
|
350
|
+
attempts += 1;
|
|
351
|
+
lastProbe = await fetcher(host, port);
|
|
352
|
+
lastCls = classifyHealth(lastProbe);
|
|
353
|
+
if (lastCls.mode === "aerial_running" || lastCls.mode === "port_conflict") {
|
|
354
|
+
return { cls: lastCls, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
|
|
355
|
+
}
|
|
356
|
+
await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
|
|
357
|
+
}
|
|
358
|
+
return { cls: lastCls || { mode: "absent" }, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function readWrapperNodePath(wrapperFile) {
|
|
362
|
+
if (!wrapperFile) return undefined;
|
|
363
|
+
try {
|
|
364
|
+
if (!fs.existsSync(wrapperFile)) return undefined;
|
|
365
|
+
const data = fs.readFileSync(wrapperFile, "utf8");
|
|
366
|
+
if (wrapperFile.endsWith(".sh")) {
|
|
367
|
+
const m = data.match(/^NODE_BIN='([^']*)'/m);
|
|
368
|
+
return m ? m[1] : undefined;
|
|
369
|
+
}
|
|
370
|
+
if (wrapperFile.endsWith(".ps1")) {
|
|
371
|
+
const m = data.match(/^\$node\s*=\s*'([^']*)'/m);
|
|
372
|
+
return m ? m[1] : undefined;
|
|
373
|
+
}
|
|
374
|
+
} catch {}
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
|
|
379
|
+
const out = {
|
|
380
|
+
stdioLog: stdioLogPath(),
|
|
381
|
+
aerialLog: aerialLogPath(),
|
|
382
|
+
wrapperNode: readWrapperNodePath(wrapper),
|
|
383
|
+
health: { attempts, elapsedMs }
|
|
384
|
+
};
|
|
385
|
+
if (probe && probe.error) out.health.lastError = probe.error;
|
|
386
|
+
if (probe && probe.status !== undefined) out.health.lastStatus = probe.status;
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
342
390
|
function authFileStatus(file) {
|
|
343
391
|
if (!fs.existsSync(file)) return { file, state: "missing" };
|
|
344
392
|
try {
|
|
@@ -461,11 +509,12 @@ function tokenWarning() {
|
|
|
461
509
|
if (data && data.trim()) return undefined;
|
|
462
510
|
} catch {}
|
|
463
511
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
512
|
+
const envToken = process.env.AERIAL_GITHUB_TOKEN;
|
|
513
|
+
const envOnly = envToken && envToken.trim();
|
|
514
|
+
const message = envOnly
|
|
515
|
+
? "AERIAL_GITHUB_TOKEN is set in this shell only; the background service does not inherit it. Run `aerial login` to persist a service-readable GitHub token, otherwise proxy requests return 503."
|
|
516
|
+
: "GitHub token is not configured. Service will start, but proxy requests return 503 until you run `aerial login`.";
|
|
517
|
+
return { level: "warning", code: "github_token_missing", message };
|
|
469
518
|
}
|
|
470
519
|
|
|
471
520
|
function darwinWriteDefinition() {
|
|
@@ -534,7 +583,7 @@ async function describeRunning(ctx, host, port, healthFetch) {
|
|
|
534
583
|
return { cls, probe, supervisor };
|
|
535
584
|
}
|
|
536
585
|
|
|
537
|
-
export async function serviceInstall({ run = defaultRunCommand, healthFetch } = {}) {
|
|
586
|
+
export async function serviceInstall({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
538
587
|
if (isUnsupportedPlatform()) throw unsupportedError("install");
|
|
539
588
|
const ctx = { run };
|
|
540
589
|
const config = loadConfig();
|
|
@@ -634,12 +683,34 @@ export async function serviceInstall({ run = defaultRunCommand, healthFetch } =
|
|
|
634
683
|
if (!ok) result.reason = "run_failed";
|
|
635
684
|
}
|
|
636
685
|
}
|
|
686
|
+
if (result.ok) {
|
|
687
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
688
|
+
result.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
|
|
689
|
+
} else {
|
|
690
|
+
const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
|
|
691
|
+
if (poll.cls.mode === "aerial_running") {
|
|
692
|
+
result.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
|
|
693
|
+
} else if (poll.cls.mode === "port_conflict") {
|
|
694
|
+
result.ok = false;
|
|
695
|
+
result.reason = "port_conflict";
|
|
696
|
+
result.message = `After install, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`;
|
|
697
|
+
result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
|
|
698
|
+
} else {
|
|
699
|
+
result.ok = false;
|
|
700
|
+
result.reason = "health_check_failed";
|
|
701
|
+
result.definitionWritten = true;
|
|
702
|
+
result.startAttempted = true;
|
|
703
|
+
result.message = `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
|
|
704
|
+
result.diagnostics = healthFailedDiagnostics({ wrapper: result.wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
637
708
|
result.warning = tokenWarning();
|
|
638
|
-
logEvent("service_install", { platform: process.platform, ok: result.ok });
|
|
709
|
+
logEvent("service_install", { platform: process.platform, ok: result.ok, reason: result.reason });
|
|
639
710
|
return result;
|
|
640
711
|
}
|
|
641
712
|
|
|
642
|
-
export async function serviceStart({ run = defaultRunCommand, healthFetch } = {}) {
|
|
713
|
+
export async function serviceStart({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
643
714
|
if (isUnsupportedPlatform()) throw unsupportedError("start");
|
|
644
715
|
const ctx = { run };
|
|
645
716
|
const config = loadConfig();
|
|
@@ -687,16 +758,50 @@ export async function serviceStart({ run = defaultRunCommand, healthFetch } = {}
|
|
|
687
758
|
} else {
|
|
688
759
|
r = ctx.run("schtasks.exe", buildSchtasksArgs("run"));
|
|
689
760
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
ok,
|
|
761
|
+
const triggerOk = r.status === 0;
|
|
762
|
+
const base = {
|
|
763
|
+
ok: triggerOk,
|
|
694
764
|
action: "start",
|
|
695
765
|
platform: process.platform,
|
|
696
766
|
status: r.status,
|
|
697
767
|
stderr: r.stderr,
|
|
698
768
|
warning: tokenWarning(),
|
|
699
|
-
...(
|
|
769
|
+
...(triggerOk ? {} : { reason: process.platform === "darwin" ? "bootstrap_failed" : "run_failed" })
|
|
770
|
+
};
|
|
771
|
+
if (!triggerOk) {
|
|
772
|
+
logEvent("service_start", { platform: process.platform, status: r.status, reason: base.reason });
|
|
773
|
+
return base;
|
|
774
|
+
}
|
|
775
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
776
|
+
base.health = { ok: true, attempts: 0, elapsedMs: 0, dryRun: true };
|
|
777
|
+
logEvent("service_start", { platform: process.platform, status: r.status, ok: true, dryRun: true });
|
|
778
|
+
return base;
|
|
779
|
+
}
|
|
780
|
+
const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
|
|
781
|
+
if (poll.cls.mode === "aerial_running") {
|
|
782
|
+
base.health = { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs };
|
|
783
|
+
logEvent("service_start", { platform: process.platform, status: r.status, ok: true });
|
|
784
|
+
return base;
|
|
785
|
+
}
|
|
786
|
+
const wrapper = process.platform === "darwin" ? darwinWrapperPath() : winWrapperPath();
|
|
787
|
+
if (poll.cls.mode === "port_conflict") {
|
|
788
|
+
logEvent("service_start", { platform: process.platform, ok: false, reason: "port_conflict" });
|
|
789
|
+
return {
|
|
790
|
+
...base,
|
|
791
|
+
ok: false,
|
|
792
|
+
reason: "port_conflict",
|
|
793
|
+
message: `After start, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
|
|
794
|
+
diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
logEvent("service_start", { platform: process.platform, ok: false, reason: "health_check_failed" });
|
|
798
|
+
return {
|
|
799
|
+
...base,
|
|
800
|
+
ok: false,
|
|
801
|
+
reason: "health_check_failed",
|
|
802
|
+
startAttempted: true,
|
|
803
|
+
message: `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`,
|
|
804
|
+
diagnostics: healthFailedDiagnostics({ wrapper, probe: poll.probe, attempts: poll.attempts, elapsedMs: poll.elapsedMs })
|
|
700
805
|
};
|
|
701
806
|
}
|
|
702
807
|
|