fix: compact noisy cli health outputs

This commit is contained in:
Codex
2026-05-22 23:00:38 +00:00
parent 79fb17b546
commit 65ac76c235
10 changed files with 454 additions and 45 deletions
+115 -17
View File
@@ -11,6 +11,7 @@ const maxTraceLimit = 500;
const defaultOutputLimit = 20;
const defaultTextPreviewChars = 12_000;
const defaultTasksLimit = 20;
const defaultQueuesLimit = 8;
const maxTasksLimit = 100;
const supervisorSectionReturnedLimit = 5;
const supervisorRecentCompletedLimit = 5;
@@ -255,7 +256,8 @@ interface CodexTasksDegraded {
interface CodexQueuesOptions {
full: boolean;
limit: number;
limitExplicit: boolean;
offset: number;
page: number;
}
interface CodexPrPreflightOptions {
@@ -593,6 +595,18 @@ function nonNegativeNumberOption(args: string[], names: string[], defaultValue:
return defaultValue;
}
function nonNegativeIntegerOption(args: string[], names: string[], defaultValue: number, maxValue = Number.MAX_SAFE_INTEGER): number {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
return Math.min(value, maxValue);
}
return defaultValue;
}
function nullablePositiveNumberOption(args: string[], names: string[]): number | null {
for (const name of names) {
const index = args.indexOf(name);
@@ -1176,6 +1190,42 @@ function compactExecutionDiagnostics(value: unknown): Record<string, unknown> |
};
}
function compactQueueExecutionDiagnostics(value: unknown): Record<string, unknown> | null {
const diagnostics = compactExecutionDiagnostics(value);
if (diagnostics === null) return null;
const listBudget = asRecord(diagnostics.listBudget) ?? {};
const omittedCounts = asRecord(listBudget.omittedCounts) ?? {};
return {
state: diagnostics.state ?? null,
degraded: diagnostics.degraded ?? null,
splitBrain: diagnostics.splitBrain ?? null,
splitBrainLive: diagnostics.splitBrainLive ?? null,
effectiveLiveness: diagnostics.effectiveLiveness ?? null,
recommendedAction: diagnostics.recommendedAction ?? null,
liveness: diagnostics.liveness ?? null,
executionStateSource: diagnostics.executionStateSource ?? null,
controlPlane: diagnostics.controlPlane ?? null,
databaseActiveTaskCount: diagnostics.databaseActiveTaskCount ?? null,
schedulerActiveRunSlotCount: diagnostics.schedulerActiveRunSlotCount ?? null,
activeHeartbeatCount: diagnostics.activeHeartbeatCount ?? null,
lastSchedulerHeartbeatAt: diagnostics.lastSchedulerHeartbeatAt ?? null,
lastObservedAgentEventAt: diagnostics.lastObservedAgentEventAt ?? null,
lastPersistedTraceAt: diagnostics.lastPersistedTraceAt ?? null,
reasons: diagnostics.reasons ?? [],
listBudget: {
truncated: listBudget.truncated ?? false,
omittedCounts: {
databaseActiveTaskIds: omittedCounts.databaseActiveTaskIds ?? 0,
activeHeartbeatTaskIds: omittedCounts.activeHeartbeatTaskIds ?? 0,
heartbeatFreshTaskIds: omittedCounts.heartbeatFreshTaskIds ?? 0,
heartbeatRiskTaskIds: omittedCounts.heartbeatRiskTaskIds ?? 0,
reasons: omittedCounts.reasons ?? 0,
},
rawCommand: listBudget.rawCommand ?? "bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview?limit=30 --raw --full",
},
};
}
function supervisorExecutionDiagnostics(value: unknown): Record<string, unknown> | null {
const diagnostics = compactExecutionDiagnostics(value);
if (diagnostics === null) return null;
@@ -1577,12 +1627,17 @@ function parseTasksOptions(args: string[]): CodexTasksOptions {
function parseQueuesOptions(args: string[]): CodexQueuesOptions {
assertKnownOptions(args, {
flags: ["--full", "--all"],
valueOptions: ["--limit"],
valueOptions: ["--limit", "--offset", "--page"],
}, "codex queues");
const limit = positiveIntegerOption(args, ["--limit"], defaultQueuesLimit, maxTasksLimit);
const page = positiveIntegerOption(args, ["--page"], 1);
const offsetExplicit = args.includes("--offset");
const offset = offsetExplicit ? nonNegativeIntegerOption(args, ["--offset"], 0) : (page - 1) * limit;
return {
full: hasFlag(args, "--full") || hasFlag(args, "--all"),
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
limitExplicit: args.includes("--limit"),
limit,
offset,
page: Math.floor(offset / limit) + 1,
};
}
@@ -2430,13 +2485,23 @@ function requireMergeTargetQueueId(args: string[], command: string): string {
return raw.trim();
}
function compactCounts(value: unknown): Record<string, unknown> {
const counts = asRecord(value) ?? {};
const compact: Record<string, unknown> = {};
for (const [key, count] of Object.entries(counts)) {
if (typeof count === "number" && count === 0) continue;
compact[key] = count;
}
return compact;
}
function compactQueueRow(value: unknown): Record<string, unknown> {
const record = asRecord(value) ?? {};
return {
id: record.id ?? null,
name: record.name ?? null,
total: record.total ?? null,
counts: record.counts ?? {},
counts: compactCounts(record.counts),
unreadTerminal: record.unreadTerminal ?? 0,
activeTaskId: record.activeTaskId ?? null,
runnableTaskId: record.runnableTaskId ?? null,
@@ -2449,6 +2514,18 @@ function compactQueueRow(value: unknown): Record<string, unknown> {
};
}
function queueListCommand(options: Partial<CodexQueuesOptions> = {}): string {
const full = options.full === true;
const limit = options.limit ?? defaultQueuesLimit;
const offset = options.offset ?? 0;
return [
"bun scripts/cli.ts codex queues",
full ? "--full" : "",
limit === defaultQueuesLimit ? "" : `--limit ${limit}`,
offset > 0 ? `--offset ${offset}` : "",
].filter(Boolean).join(" ");
}
function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueuesOptions, upstream: { ok: unknown; status: unknown }): Record<string, unknown> {
const queue = asRecord(body.queue) ?? asRecord(body.summary) ?? {};
const queues = asArray(body.queues).map(compactQueueRow);
@@ -2458,17 +2535,31 @@ function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueu
const runnableQueues = queues.filter((row) => row.runnableTaskId !== null && row.runnableTaskId !== undefined);
const activeQueues = queues.filter((row) => typeof row.id === "string" && activeIds.includes(row.id));
const selected = options.full ? queues : Array.from(new Map([...activeQueues, ...unreadQueues, ...runnableQueues, ...nonemptyQueues].map((row) => [String(row.id), row])).values());
const limitApplied = !options.full || options.limitExplicit;
const visible = limitApplied ? selected.slice(0, options.limit) : selected;
const diagnostics = compactExecutionDiagnostics(queue.executionDiagnostics);
const visible = selected.slice(options.offset, options.offset + options.limit);
const diagnostics = compactQueueExecutionDiagnostics(queue.executionDiagnostics);
const activeTaskIds = boundedUniqueStringList(queue.activeTaskIds, Math.min(options.limit, maxTasksLimit));
const queuedTaskIds = boundedUniqueStringList(queue.queuedTaskIds, Math.min(options.limit, maxTasksLimit));
const nextOffset = options.offset + visible.length;
const previousOffset = Math.max(0, options.offset - options.limit);
const hasMore = nextOffset < selected.length;
const hasPrevious = options.offset > 0;
return {
upstream,
queues: {
view: options.full ? "full" : "summary",
bounded: limitApplied,
bounded: true,
outputPolicy: {
default: "paged-low-noise",
stableItemsPath: "data.queues.items[]",
rawFullCommand: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw --full",
},
count: selected.length,
returned: visible.length,
hasMore: selected.length > visible.length,
limit: options.limit,
offset: options.offset,
page: options.page,
hasMore,
hasPrevious,
totals: {
totalTasks: queue.total ?? null,
queueCount: queue.queueCount ?? queues.length,
@@ -2478,29 +2569,36 @@ function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueu
runnableQueueCount: runnableQueues.length,
},
activeQueueIds: queue.activeQueueIds ?? [],
activeTaskIds: queue.activeTaskIds ?? [],
queuedTaskIds: queue.queuedTaskIds ?? [],
activeTaskIds: activeTaskIds.items,
activeTaskIdsCount: activeTaskIds.count,
activeTaskIdsTruncated: activeTaskIds.truncated,
queuedTaskIds: queuedTaskIds.items,
queuedTaskIdsCount: queuedTaskIds.count,
queuedTaskIdsTruncated: queuedTaskIds.truncated,
counts: queue.counts ?? {},
unreadTerminal: queue.unreadTerminal ?? 0,
executionDiagnostics: diagnostics,
items: visible,
...(options.full
? {
deprecatedFullArray: asArray(body.queues),
compatibility: {
deprecated: true,
deprecatedPath: "data.queues.deprecatedFullArray[]",
stablePath: "data.queues.items[]",
message: "Use data.queues.items[] for both codex queues and codex queues --full.",
deprecatedFullArrayOmitted: true,
message: "Use data.queues.items[] for both codex queues and codex queues --full; raw full upstream is available only through the explicit raw command.",
},
}
: {}),
commands: {
refresh: `bun scripts/cli.ts codex queues${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
full: `bun scripts/cli.ts codex queues --full${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
refresh: queueListCommand({ full: options.full, limit: options.limit, offset: options.offset }),
next: hasMore ? queueListCommand({ full: options.full, limit: options.limit, offset: nextOffset }) : null,
previous: hasPrevious ? queueListCommand({ full: options.full, limit: options.limit, offset: previousOffset }) : null,
first: queueListCommand({ full: options.full, limit: options.limit, offset: 0 }),
full: queueListCommand({ full: true, limit: options.limit, offset: 0 }),
tasks: `bun scripts/cli.ts codex tasks --view supervisor --limit ${Math.min(options.limit, defaultTasksLimit)}`,
unread: `bun scripts/cli.ts codex tasks --unread --limit ${Math.min(options.limit, defaultTasksLimit)}`,
raw: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw",
raw: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw --full",
},
},
};
+8 -8
View File
@@ -17,7 +17,7 @@ export function rootHelp(): unknown {
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
{ command: "server cleanup plan [--min-age-hours N] [--limit N]", description: "Dry-run Docker image cleanup plan only: list active/protected images, stale candidates older than the default 24h threshold, risk, estimated reclaim, and manual review commands without deleting anything." },
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Maintenance-only local Compose rebuild for reviewed main-server services; frontend standard release must use CI artifact plus deploy apply dev/prod artifact consumers." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...]", description: "Generate the minimal external provider-gateway env/compose bundle or run the read-only provider health triage contract." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge with built-in remote helper tools in PATH." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
@@ -27,9 +27,9 @@ export function rootHelp(): unknown {
{ command: "ssh <providerId> argv <command> [args...]", description: "Run a remote command with each argv token shell-quoted by UniDesk before SSH passthrough." },
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
{ command: "microservice health <id>", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy." },
{ command: "microservice health <id> [--full|--raw]", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy; default output is a compact health summary." },
{ command: "microservice proxy <id> <path> [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; JSON request bodies are supported for controlled write/debug endpoints." },
{ command: "microservice diagnostics <id>", description: "Split k3sctl-managed proxy health into provider-gateway, HTTP tunnel, adapter, Kubernetes API service proxy, and target Service checks." },
{ command: "microservice diagnostics <id> [--full|--raw]", description: "Split k3sctl-managed proxy health into a compact summary by default; use --full/--raw for complete evidence." },
{ command: "microservice tunnel-self-test <id>", description: "Trigger an expected provider HTTP tunnel failure and verify requestId/stage diagnostics are returned." },
{ command: "decision upload <markdown-file> [--title text] [--type meeting|decision|goal|external_goal|internal_goal|blocker|debt|experiment] [--level|--priority G0|G1|G2|G3|P0|P1|P2|P3|none] [--doc-no DC-...] [--doc-type DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS] [--doc-priority P0|P1|P2|P3] [--signer text] [--issued-at ISO]", description: "Upload a meeting note or decision/requirement record through backend-core -> decision-center user-service proxy." },
{ command: "decision diary import <markdown-file> [--source-file path] [--tag tag] [--include-entries]", description: "Import a dated work log Markdown into PostgreSQL diary entries split as YYYY-MM/YYYY-MM-DD.md." },
@@ -165,9 +165,9 @@ function microserviceHelp(): unknown {
output: "json",
usage: [
"bun scripts/cli.ts microservice list",
"bun scripts/cli.ts microservice status <id>",
"bun scripts/cli.ts microservice health <id>",
"bun scripts/cli.ts microservice diagnostics <id>",
"bun scripts/cli.ts microservice status <id> [--full|--raw]",
"bun scripts/cli.ts microservice health <id> [--full|--raw]",
"bun scripts/cli.ts microservice diagnostics <id> [--full|--raw]",
"bun scripts/cli.ts microservice tunnel-self-test <id>",
"bun scripts/cli.ts microservice proxy <id> <path> [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--full] [--max-body-bytes N]",
],
@@ -197,7 +197,7 @@ function providerHelp(): unknown {
output: "json",
usage: [
"bun scripts/cli.ts provider attach <providerId> [--master-server URL] [--up] [--force]",
"bun scripts/cli.ts provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...]",
"bun scripts/cli.ts provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]",
],
description: "Generate the minimal provider-gateway attach env/compose bundle or run the read-only provider health triage contract.",
};
@@ -260,7 +260,7 @@ function codexHelp(): unknown {
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]",
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
"bun scripts/cli.ts codex queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>",
"bun scripts/cli.ts codex queues [--full|--all] [--limit N] [--page N|--offset N] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>",
],
promptInput: {
recommended: ["--prompt-stdin", "--prompt-file"],
+147 -3
View File
@@ -240,6 +240,14 @@ function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function parseJsonOption(raw: string, name: string): unknown {
try {
return JSON.parse(raw) as unknown;
@@ -264,6 +272,14 @@ function requestBodyOption(args: string[]): unknown | undefined {
return undefined;
}
function assertKnownObservationOptions(args: string[], command: string): void {
const flags = new Set(["--full", "--raw"]);
for (const arg of args) {
if (!arg.startsWith("--")) continue;
if (!flags.has(arg)) throw new Error(`unsupported ${command} option: ${arg}`);
}
}
function methodOption(args: string[], hasBody = false): string {
const method = (stringOption(args, "--method") ?? (hasBody ? "POST" : "GET")).toUpperCase();
if (!["GET", "HEAD", "POST", "DELETE", "PUT", "PATCH"].includes(method)) throw new Error(`unsupported --method ${method}`);
@@ -314,20 +330,148 @@ export function summarizeMicroserviceProxyResponse(response: unknown, args: stri
};
}
function compactStringList(value: unknown, limit = 8): Record<string, unknown> {
const all = Array.from(new Set(asArray(value).map((item) => String(item ?? "")).filter(Boolean)));
return {
items: all.slice(0, limit),
count: all.length,
truncated: all.length > limit,
omitted: Math.max(0, all.length - limit),
};
}
function compactRecordFields(record: Record<string, unknown> | null, keys: string[]): Record<string, unknown> | null {
if (record === null) return null;
const selected: Record<string, unknown> = {};
for (const key of keys) {
if (record[key] !== undefined) selected[key] = record[key];
}
return Object.keys(selected).length > 0 ? selected : null;
}
function compactQueueHealth(value: unknown): Record<string, unknown> | null {
const queue = asRecord(value);
if (queue === null) return null;
return {
controlPlane: queue.controlPlane ?? null,
defaultProviderId: queue.defaultProviderId ?? null,
defaultModel: queue.defaultModel ?? null,
counts: queue.counts ?? {},
total: queue.total ?? null,
queueCount: queue.queueCount ?? null,
activeQueueIds: compactStringList(queue.activeQueueIds, 8),
activeTaskIds: compactStringList(queue.activeTaskIds ?? queue.databaseActiveTaskIds, 8),
queuedTaskIds: compactStringList(queue.queuedTaskIds, 8),
databaseActiveTaskCount: queue.databaseActiveTaskCount ?? null,
schedulerActiveRunSlotCount: queue.schedulerActiveRunSlotCount ?? queue.activeRunSlotCount ?? null,
};
}
function compactMicroserviceBody(body: unknown, serviceId: string): Record<string, unknown> | null {
const record = asRecord(body);
if (record === null) return null;
const diagnostics = asRecord(record.executionDiagnostics) ?? asRecord(record.diagnostics);
const devReady = asRecord(record.devReady);
return {
ok: record.ok ?? null,
serviceId: record.serviceId ?? record.service ?? serviceId,
service: record.service ?? null,
role: record.role ?? null,
status: record.status ?? null,
healthy: record.healthy ?? null,
mode: record.mode ?? null,
environment: record.environment ?? null,
version: record.version ?? record.gatewayVersion ?? null,
target: record.target ?? record.k3sServiceId ?? null,
updatedAt: record.updatedAt ?? record.observedAt ?? record.generatedAt ?? null,
startedAt: record.startedAt ?? null,
taskCount: record.taskCount ?? null,
schemaReady: record.schemaReady ?? null,
queue: compactQueueHealth(record.queue),
resourceBudget: compactRecordFields(asRecord(record.resourceBudget), [
"targetMemoryMb",
"mgrPoolMax",
"tracePoolMax",
"noRunnerDependencies",
"noDockerSocket",
"noPlaywright",
"rustRewrite",
]),
topLevelKeys: compactStringList(Object.keys(record), 16),
devReady: devReady === null ? null : {
ok: devReady.ok ?? null,
missingTools: compactStringList(devReady.missingTools, 8),
skills: devReady.skills ?? null,
},
executionDiagnostics: diagnostics === null ? null : {
state: diagnostics.state ?? null,
effectiveLiveness: diagnostics.effectiveLiveness ?? null,
recommendedAction: diagnostics.recommendedAction ?? null,
splitBrainLive: diagnostics.splitBrainLive ?? null,
schedulerActiveRunSlotCount: diagnostics.schedulerActiveRunSlotCount ?? null,
databaseActiveTaskCount: diagnostics.databaseActiveTaskCount ?? null,
heartbeatFreshTaskIds: compactStringList(diagnostics.heartbeatFreshTaskIds, 8),
heartbeatRiskTaskIds: compactStringList(diagnostics.heartbeatRiskTaskIds, 8),
},
};
}
function summarizeMicroserviceObservation(action: string, serviceId: string, response: unknown, args: string[]): unknown {
if (hasFlag(args, "--full") || hasFlag(args, "--raw")) return response;
const record = asRecord(response);
if (record === null) return response;
const body = "body" in record ? record.body : response;
const bodyBytes = jsonByteLength(body);
const summary = compactMicroserviceBody(body, serviceId);
const includePreview = summary === null || bodyBytes <= 20_000;
return {
upstream: {
ok: record.ok ?? null,
status: record.status ?? null,
exitCode: record.exitCode ?? null,
},
microservice: {
action,
id: serviceId,
summary,
bodyBytes,
bodyOmitted: true,
outputPolicy: {
default: "compact-health-summary",
full: `bun scripts/cli.ts microservice ${action} ${serviceId} --full`,
raw: `bun scripts/cli.ts microservice ${action} ${serviceId} --raw`,
proxyRaw: serviceId === "code-queue" && action === "health" ? "bun scripts/cli.ts microservice proxy code-queue /health --raw --full" : null,
},
...(includePreview
? { bodyPreview: previewJson(body, { maxDepth: 2, maxArrayItems: 2, maxObjectKeys: 8, maxStringLength: 160 }) }
: {
bodyPreviewOmitted: true,
bodyPreviewHint: "Large body preview omitted because compact summary is available; use --full or --raw for complete evidence.",
}),
},
};
}
export async function runMicroserviceCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "list", idArg, pathArg] = args;
if (action === "list") return coreInternalFetch("/api/microservices");
if (action === "status") {
const id = requireId(idArg, "microservice status");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/status`);
const optionArgs = args.slice(2);
assertKnownObservationOptions(optionArgs, "microservice status");
return summarizeMicroserviceObservation(action, id, coreInternalFetch(`/api/microservices/${encodeId(id)}/status`), optionArgs);
}
if (action === "health") {
const id = requireId(idArg, "microservice health");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/health`);
const optionArgs = args.slice(2);
assertKnownObservationOptions(optionArgs, "microservice health");
return summarizeMicroserviceObservation(action, id, coreInternalFetch(`/api/microservices/${encodeId(id)}/health`), optionArgs);
}
if (action === "diagnostics") {
const id = requireId(idArg, "microservice diagnostics");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/diagnostics`);
const optionArgs = args.slice(2);
assertKnownObservationOptions(optionArgs, "microservice diagnostics");
return summarizeMicroserviceObservation(action, id, coreInternalFetch(`/api/microservices/${encodeId(id)}/diagnostics`), optionArgs);
}
if (action === "tunnel-self-test") {
const id = requireId(idArg, "microservice tunnel-self-test");
+26 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { buildProviderTriageResult, type ProviderTriageSignal } from "./provider-triage";
import { buildProviderTriageResult, compactProviderTriageResult, type ProviderTriageSignal } from "./provider-triage";
function signal(
id: string,
@@ -77,4 +77,29 @@ describe("provider triage contract", () => {
expect(result.degradedScopes).toContain("registry");
expect(result.healthyScopes).toEqual(expect.arrayContaining(["k3s", "provider-gateway", "ssh"]));
});
test("compact output is bounded and preserves drill-down command args", () => {
const result = buildProviderTriageResult("D601", [
signal("backend-core-node", "provider-gateway", "ok"),
signal("host-ssh-probe", "ssh", "ok"),
signal("artifact-registry-health", "registry", "degraded"),
signal("k3sctl-adapter-health", "k3s", "unknown"),
signal("code-queue-health", "scheduler", "unknown"),
signal("service-a", "microservice", "failed"),
signal("service-b", "microservice", "failed"),
signal("service-c", "microservice", "degraded"),
signal("service-d", "microservice", "unknown"),
signal("service-e", "microservice", "unknown"),
signal("service-f", "microservice", "unknown"),
], "2026-05-20T00:00:00.000Z");
const compact = compactProviderTriageResult(result, ["--microservice", "code-queue", "--microservice", "k3sctl-adapter"]);
const signalCounts = compact.signalCounts as Record<string, unknown>;
const outputPolicy = compact.outputPolicy as Record<string, unknown>;
expect((compact.signals as unknown[]).length).toBe(8);
expect(signalCounts.omittedIssueSignals).toBe(1);
expect(outputPolicy.full).toBe("bun scripts/cli.ts provider triage D601 --microservice 'code-queue' --microservice 'k3sctl-adapter' --full");
expect(outputPolicy.raw).toBe("bun scripts/cli.ts provider triage D601 --microservice 'code-queue' --microservice 'k3sctl-adapter' --raw");
});
});
+134 -2
View File
@@ -87,6 +87,14 @@ function bool(value: unknown): boolean {
return value === true;
}
function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function lower(value: unknown): string {
return String(value ?? "").toLowerCase();
}
@@ -292,6 +300,127 @@ function observedErrorSignal(message: string, scope: ProviderSignalScope): Provi
return signal("observed-error", scope, "failed", message, { message, runnerErrorClassification: classifyRunnerError(message) }, scope !== "runner-local");
}
function compactStringList(value: unknown, limit = 6): Record<string, unknown> {
const all = Array.from(new Set(asArray(value).map((item) => String(item ?? "")).filter(Boolean)));
return {
items: all.slice(0, limit),
count: all.length,
truncated: all.length > limit,
omitted: Math.max(0, all.length - limit),
};
}
function compactEvidence(value: unknown): unknown {
const record = asRecord(value);
if (record === null) return value;
const body = bodyOf(value);
const devReady = asRecord(record.devReady) ?? asRecord(body?.devReady);
const diagnostics = asRecord(record.executionDiagnostics) ?? asRecord(body?.executionDiagnostics) ?? asRecord(asRecord(value)?.diagnostics);
return {
upstream: record.upstream ?? (body === null ? null : { ok: asRecord(value)?.ok ?? null, status: asRecord(value)?.status ?? null }),
status: record.status ?? body?.status ?? null,
ok: record.ok ?? body?.ok ?? null,
serviceId: record.serviceId ?? body?.serviceId ?? null,
providerGatewayVersion: record.providerGatewayVersion ?? null,
hostSshConfigured: record.hostSshConfigured ?? null,
taskId: record.taskId ?? null,
taskStatus: record.taskStatus ?? null,
exitCode: record.exitCode ?? null,
devReady: devReady === null ? null : {
ok: devReady.ok ?? null,
missingTools: compactStringList(devReady.missingTools),
},
executionDiagnostics: diagnostics === null ? null : {
state: diagnostics.state ?? null,
effectiveLiveness: diagnostics.effectiveLiveness ?? null,
recommendedAction: diagnostics.recommendedAction ?? null,
splitBrainLive: diagnostics.splitBrainLive ?? null,
heartbeatFreshTaskIds: compactStringList(diagnostics.heartbeatFreshTaskIds),
heartbeatRiskTaskIds: compactStringList(diagnostics.heartbeatRiskTaskIds),
},
fallback: record.fallback === undefined ? null : record.fallback,
error: record.error ?? null,
};
}
function providerTriageCommand(providerId: string, args: string[], mode: "--full" | "--raw"): string {
const kept: string[] = [];
const valueOptions = new Set(["--observed-error", "--observed-scope", "--microservice", "--service", "--microservices"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--full" || arg === "--raw") continue;
if (valueOptions.has(arg)) {
const value = args[index + 1];
if (value !== undefined) {
kept.push(arg, shellQuote(value));
index += 1;
}
continue;
}
kept.push(arg);
}
return [`${commandPrefix} provider triage ${providerId}`, ...kept, mode].filter(Boolean).join(" ");
}
export function compactProviderTriageResult(result: ProviderTriageResult, args: string[] = []): Record<string, unknown> {
const issueSignals = result.signals
.filter((item) => item.status === "failed" || item.status === "degraded" || item.status === "unknown")
.sort((left, right) => {
const rank: Record<ProviderSignalStatus, number> = { failed: 0, degraded: 1, unknown: 2, ok: 3 };
return rank[left.status] - rank[right.status];
});
const sourceSignals = issueSignals.length > 0 ? issueSignals : [];
const signalLimit = issueSignals.length > 0 ? 8 : 0;
const visibleSignals = sourceSignals.slice(0, signalLimit);
const okSignalCount = result.signals.filter((item) => item.status === "ok").length;
const issueSignalCount = issueSignals.length;
return {
ok: result.ok,
providerId: result.providerId,
decision: result.decision,
scope: result.scope,
retryable: result.retryable,
blockingDisposition: result.blockingDisposition,
observedAt: result.observedAt,
failedScopes: result.failedScopes,
degradedScopes: result.degradedScopes,
healthyScopes: result.healthyScopes,
failedIndependentScopes: result.failedIndependentScopes,
healthyIndependentScopes: result.healthyIndependentScopes,
rationale: result.rationale,
signalCounts: {
total: result.signals.length,
returned: visibleSignals.length,
limit: signalLimit,
ok: okSignalCount,
degraded: result.signals.filter((item) => item.status === "degraded").length,
failed: result.signals.filter((item) => item.status === "failed").length,
unknown: result.signals.filter((item) => item.status === "unknown").length,
omittedOkSignals: Math.max(0, result.signals.filter((item) => item.status === "ok").length - visibleSignals.filter((item) => item.status === "ok").length),
omittedIssueSignals: Math.max(0, issueSignalCount - visibleSignals.filter((item) => item.status === "failed" || item.status === "degraded" || item.status === "unknown").length),
omittedSignals: Math.max(0, sourceSignals.length - visibleSignals.length),
},
signals: visibleSignals.map((item) => ({
id: item.id,
scope: item.scope,
status: item.status,
independentPath: item.independentPath,
observedAt: item.observedAt,
summary: item.summary,
evidenceSummary: compactEvidence(item.evidence),
})),
recommendedCrossChecks: result.recommendedCrossChecks.slice(0, 8),
outputPolicy: {
default: "compact-triage-summary",
signalLimit,
full: providerTriageCommand(result.providerId, args, "--full"),
raw: providerTriageCommand(result.providerId, args, "--raw"),
note: "Default output returns prioritized failed/degraded/unknown signals plus bounded evidence. Use --full or --raw only when complete evidence is required.",
},
contract: result.contract,
};
}
export function providerTriageRecommendedCrossChecks(providerId: string): string[] {
return [
`${commandPrefix} provider triage ${providerId}`,
@@ -429,10 +558,12 @@ function optionValue(args: string[], name: string): string | undefined {
}
function assertKnownOptions(args: string[]): void {
const flags = new Set(["--full", "--raw"]);
const valueOptions = new Set(["--observed-error", "--observed-scope", "--microservice", "--service", "--microservices"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (!arg.startsWith("--")) continue;
if (flags.has(arg)) continue;
if (!valueOptions.has(arg)) throw new Error(`unsupported provider triage option: ${arg}`);
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
@@ -440,7 +571,7 @@ function assertKnownOptions(args: string[]): void {
}
}
export async function runProviderTriage(config: UniDeskConfig, providerId: string, args: string[] = []): Promise<ProviderTriageResult> {
export async function runProviderTriage(config: UniDeskConfig, providerId: string, args: string[] = []): Promise<unknown> {
if (!/^[A-Za-z0-9_.-]{1,64}$/u.test(providerId)) throw new Error("provider triage requires a safe provider id such as D601");
assertKnownOptions(args);
const observedAt = isoNow();
@@ -497,5 +628,6 @@ export async function runProviderTriage(config: UniDeskConfig, providerId: strin
}
}
return buildProviderTriageResult(providerId, signals, observedAt);
const result = buildProviderTriageResult(providerId, signals, observedAt);
return hasFlag(args, "--full") || hasFlag(args, "--raw") ? result : compactProviderTriageResult(result, args);
}