Improve setup daemon diagnostics

This commit is contained in:
Andrey Avtomonov 2026-05-25 16:14:22 +02:00
parent 853ab10be2
commit 3a3086b7cd
11 changed files with 236 additions and 24 deletions

View file

@ -228,11 +228,14 @@ async function runStageTwoTreePicker(input: {
? initialSelectionForExisting(args.existing.enabledTables, byId)
: initialSelectionFromDefaults(selectedSchemas, schemaIds);
const initialState = buildInitialState({
tree,
existingSelectedIds: initialSelection,
skipEmptyAction: 'save-empty',
});
const initialState = {
...buildInitialState({
tree,
existingSelectedIds: initialSelection,
skipEmptyAction: 'save-empty',
}),
expanded: new Set(schemaIds),
};
const schemaWordPlural = schemaCount === 1 ? args.schemaNoun : args.schemaNounPlural;
const subtitleLines = [

View file

@ -0,0 +1,28 @@
export function describeError(error: unknown): string {
if (!(error instanceof Error)) {
const text = String(error);
return text.length > 0 ? text : 'unknown error';
}
const parts: string[] = [];
if (error.message.length > 0) {
parts.push(error.message);
}
const seen = new Set<unknown>([error]);
let cause: unknown = error.cause;
while (cause && !seen.has(cause)) {
seen.add(cause);
if (cause instanceof Error) {
if (cause.message.length > 0) {
parts.push(cause.message);
}
cause = cause.cause;
} else {
const text = String(cause);
if (text.length > 0) {
parts.push(text);
}
break;
}
}
return parts.length > 0 ? parts.join(': ') : 'unknown error';
}

View file

@ -1,3 +1,4 @@
import { describeError } from '../error-message.js';
import { createKtxEmbeddingProvider, type KtxEmbeddingProviderDeps } from './embedding-provider.js';
import type { KtxEmbeddingConfig } from './types.js';
@ -48,7 +49,6 @@ export async function runKtxEmbeddingHealthCheck(
}
return { ok: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { ok: false, message: redactHealthCheckMessage(message, config) };
return { ok: false, message: redactHealthCheckMessage(describeError(error), config) };
}
}

View file

@ -4,6 +4,7 @@ import { createServer } from 'node:net';
import { setTimeout as delay } from 'node:timers/promises';
import { promisify } from 'node:util';
import { z } from 'zod';
import { describeError } from './error-message.js';
import {
installManagedPythonRuntime,
managedPythonDaemonLayout,
@ -16,6 +17,17 @@ import {
} from './managed-python-runtime.js';
import { sanitizeChildProxyEnv } from './proxy-env.js';
export class ManagedPythonDaemonStartError extends Error {
readonly detail: string;
readonly stderrLog: string;
constructor(detail: string, stderrLog: string) {
super(`KTX daemon failed to start: ${detail}. stderr: ${stderrLog}`);
this.name = 'ManagedPythonDaemonStartError';
this.detail = detail;
this.stderrLog = stderrLog;
}
}
export interface ManagedPythonDaemonState {
schemaVersion: 1;
pid: number;
@ -237,7 +249,7 @@ async function healthOk(input: {
}
return { ok: true };
} catch (error) {
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
return { ok: false, detail: describeError(error) };
}
}
@ -328,7 +340,7 @@ async function waitForHealth(input: {
return;
}
lastDetail = finalHealth.detail;
throw new Error(`KTX daemon failed to start: ${lastDetail}. stderr: ${input.state.stderrLog}`);
throw new ManagedPythonDaemonStartError(lastDetail, input.state.stderrLog);
}
async function removeState(layout: ManagedPythonDaemonLayout): Promise<void> {
@ -721,13 +733,21 @@ export async function startManagedPythonDaemon(
stdoutLog: layout.daemonStdoutPath,
stderrLog: layout.daemonStderrPath,
};
await waitForHealth({
state,
cliVersion: options.cliVersion,
fetch: fetchImpl,
timeoutMs: options.startupTimeoutMs ?? 10_000,
pollIntervalMs: options.pollIntervalMs ?? 100,
});
try {
await waitForHealth({
state,
cliVersion: options.cliVersion,
fetch: fetchImpl,
timeoutMs: options.startupTimeoutMs ?? 30_000,
pollIntervalMs: options.pollIntervalMs ?? 100,
});
} catch (error) {
if (processAlive(state.pid)) {
killProcess(state.pid);
}
await removeState(layout);
throw error;
}
await writeState(layout.daemonStatePath, state);
return { status: 'started', layout, state, baseUrl: baseUrl(state) };
} finally {

View file

@ -113,13 +113,13 @@ export interface KtxSetupDatabasesDeps {
}
const DRIVER_OPTIONS: Array<{ value: KtxSetupDatabaseDriver; label: string }> = [
{ value: 'sqlite', label: 'SQLite' },
{ value: 'postgres', label: 'PostgreSQL' },
{ value: 'bigquery', label: 'BigQuery' },
{ value: 'snowflake', label: 'Snowflake' },
{ value: 'mysql', label: 'MySQL' },
{ value: 'clickhouse', label: 'ClickHouse' },
{ value: 'sqlserver', label: 'SQL Server' },
{ value: 'bigquery', label: 'BigQuery' },
{ value: 'snowflake', label: 'Snowflake' },
{ value: 'sqlite', label: 'SQLite' },
];
const DRIVER_LABELS = Object.fromEntries(DRIVER_OPTIONS.map((option) => [option.value, option.label])) as Record<

View file

@ -12,6 +12,7 @@ import {
managedLocalEmbeddingHealthConfig,
type ManagedLocalEmbeddingsDaemon,
} from './managed-local-embeddings.js';
import { ManagedPythonDaemonStartError } from './managed-python-daemon.js';
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
import { withTextInputNavigation } from './prompt-navigation.js';
import { envCredentialReference, writeProjectLocalSecretReference } from './setup-secrets.js';
@ -419,7 +420,12 @@ export async function runKtxSetupEmbeddingsStep(
io,
});
} catch (error) {
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
if (error instanceof ManagedPythonDaemonStartError) {
const tail = await readLocalEmbeddingDaemonStderrTail(error.stderrLog);
io.stderr.write(`${localEmbeddingSetupMessage(error.detail, tail)}\n`);
} else {
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
}
return { status: 'failed', projectDir: args.projectDir };
}
}