fix: compact noisy cli health outputs
This commit is contained in:
+115
-17
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user