Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/cost/pricing-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export function schedulePricingMetadataRefresh(options: PricingFetcherOptions =
.then((cache) => {
if (cache !== null) applyPricingCacheMetadata(cache);
})
.catch(() => undefined);
.catch((err: unknown) => {
// Match pricing-fetcher: keep refresh best-effort, surface the failure.
process.stderr.write(`pricing-metadata: refresh error: ${err}\n`);
});
}

export async function bootstrapPricingMetadata(options: PricingFetcherOptions = {}): Promise<void> {
Expand Down
74 changes: 64 additions & 10 deletions src/exec/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ import { getToolApprovalBudget } from "../tui/tool-execution-watchdog.js";

const logger = getLogger([LOG_NAMESPACE_ROOT, "exec"]);

/** Normalize unknown catch values for structured warn/error logs. */
export function formatCaughtError(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}

/** Content-less inbound used after compact so the reactor re-enters (matches TUI). */
function buildCompactionContinuationMessage(): InboundMessage {
return {
Expand Down Expand Up @@ -194,7 +199,15 @@ export async function runExec(config: Config): Promise<ExecResult> {
mcpServers: connectedMcp,
...(status !== "running" ? { finishedAt: Date.now() } : {}),
...(extra?.error !== undefined ? { error: extra.error } : {}),
}).catch(() => undefined);
}).catch((err: unknown) => {
// Persistence failure must not fail the run, but dropping it silently
// hides disk/permission problems that leave run.json stale.
logger.warn("saveState failed for session {sessionId} status={status}: {error}", {
sessionId,
status,
error: formatCaughtError(err),
});
});
};


Expand All @@ -204,7 +217,12 @@ export async function runExec(config: Config): Promise<ExecResult> {
try {
const inferenceDeps = await createInferenceDependencies();
await seedPricingMetadataFromCache({ cachePath: defaultPricingCachePath() }).catch(
() => undefined,
(err: unknown) => {
// Pricing seed is optional for exec; continue without rates rather than fail the run.
logger.debug("seedPricingMetadataFromCache failed: {error}", {
error: formatCaughtError(err),
});
},
);

let projectTrust = await loadProjectTrust(config.cwd);
Expand Down Expand Up @@ -243,8 +261,14 @@ export async function runExec(config: Config): Promise<ExecResult> {
const profilesDir = join(config.cwd, ".agents", "agents");
const liveAgentProfiles = await loadAgentProfiles(profilesDir);

// loadLocalSettings maps ENOENT → null; a throw is real I/O or schema failure.
const localSettingsForMode = await loadLocalSettings(localSettingsPath(config.cwd)).catch(
() => null,
(err: unknown) => {
logger.warn("Failed to load local settings: {error}", {
error: formatCaughtError(err),
});
return null;
},
);
const sessionMode: SessionMode =
resolveSessionMode(config.settings, localSettingsForMode) ?? "orchestrator";
Expand Down Expand Up @@ -590,17 +614,33 @@ export async function runExec(config: Config): Promise<ExecResult> {
// the entire successful reply as a spurious partial. After the drain,
// dispose is a no-op on success and a real record only if a cycle
// died without a terminal event.
await activeAgent.close().catch(() => undefined);
await streamPromise.catch(() => undefined);
await activeAgent.close().catch((err: unknown) => {
logger.debug("agent.close during successful-send teardown failed: {error}", {
error: formatCaughtError(err),
});
});
await streamPromise.catch((err: unknown) => {
logger.debug("stream drain during successful-send teardown failed: {error}", {
error: formatCaughtError(err),
});
});
await cycleRecorder.dispose("cancelled");
} else {
// Failed or aborted send: close() tears down stream consumers before
// the dead cycle's inference.error is delivered, so dispose (which
// snapshots the buffer at entry) runs before closing or the text is
// lost.
await cycleRecorder.dispose(sendCompleted ? "cancelled" : "send-failed");
await activeAgent.close().catch(() => undefined);
await streamPromise.catch(() => undefined);
await activeAgent.close().catch((err: unknown) => {
logger.debug("agent.close during failed-send teardown failed: {error}", {
error: formatCaughtError(err),
});
});
await streamPromise.catch((err: unknown) => {
logger.debug("stream drain during failed-send teardown failed: {error}", {
error: formatCaughtError(err),
});
});
}
}

Expand Down Expand Up @@ -630,7 +670,13 @@ export async function runExec(config: Config): Promise<ExecResult> {
toolCallCount: runSink.getToolCallCount(),
...(runError !== undefined ? { error: runError } : {}),
});
await hookManager.dispatchPostRun(runSummary).catch(() => undefined);
await hookManager.dispatchPostRun(runSummary).catch((err: unknown) => {
// Post-run hooks are best-effort; keep the exec exit path intact but
// surface the failure so operators can see hook/script problems.
const message = formatCaughtError(err);
logger.warn("dispatchPostRun failed: {error}", { error: message });
stderr.write(`Warning: post-run hook failed: ${message}\n`);
});

if (!sendCompleted || runError !== undefined || summaryStatus === "failed") {
const message =
Expand Down Expand Up @@ -688,11 +734,19 @@ export async function runExec(config: Config): Promise<ExecResult> {
};
} finally {
if (agent !== null) {
await agent.close().catch(() => undefined);
await agent.close().catch((err: unknown) => {
logger.debug("agent.close during exec finally failed: {error}", {
error: formatCaughtError(err),
});
});
}
// Match TUI: always dispose toolset (MCP clients + posix/plugin resources).
if (toolset !== null) {
await toolset.dispose().catch(() => undefined);
await toolset.dispose().catch((err: unknown) => {
logger.debug("toolset.dispose during exec finally failed: {error}", {
error: formatCaughtError(err),
});
});
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,16 @@ async function writeProfileFile(dir: string, profile: AgentProfile): Promise<voi
}

async function deleteProfileFile(dir: string, id: string): Promise<void> {
await unlink(`${dir}/${id}.json`).catch(() => {});
try {
await unlink(`${dir}/${id}.json`);
} catch (err: unknown) {
// Missing file is the common case (already deleted / never written).
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
getLogger([LOG_NAMESPACE_ROOT, "tui", "profiles"]).warn(
"Failed to delete agent profile {id}: {error}",
{ id, error: err instanceof Error ? err.message : String(err) },
);
}
}

export type AppProps = {
Expand Down
24 changes: 22 additions & 2 deletions src/tui/hooks/use-provider-auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, useState, type Dispatch, type SetStateAction } from "react";
import { getLogger } from "@intx/log";
import { getValidCodexToken, CodexAuthError } from "../../auth/codex/session.js";
import { refreshCodexInstructions } from "../../auth/codex/instructions.js";
import { removeCodexProfile } from "../../auth/codex/store.js";
Expand All @@ -10,6 +11,9 @@ import { codexProviderName, codexProfileFromProviderName } from "../../config/co
import { xaiProviderName, xaiProfileFromProviderName } from "../../config/xai-providers.js";
import { fetchCodexModels } from "../../auth/codex/usage.js";
import type { ProviderCatalogEntry } from "../../config/index.js";
import { LOG_NAMESPACE_ROOT } from "../../branding.js";

const logger = getLogger([LOG_NAMESPACE_ROOT, "tui", "provider-auth"]);

export type LoginModal = "codex" | "xai" | "choose" | null;

Expand Down Expand Up @@ -137,8 +141,24 @@ export function useProviderAuth({
};

const switchToCodexProfile = (name: string): void => {
void refreshCodexInstructions().catch(() => {});
void Promise.all([getValidCodexToken(name), fetchCodexModels(name).catch(() => [])]).then(
void refreshCodexInstructions().catch((err: unknown) => {
// Best-effort prompt refresh; profile switch still proceeds with cached text.
logger.warn("Codex instructions refresh failed: {error}", {
error: err instanceof Error ? err.message : String(err),
});
});
void Promise.all([
getValidCodexToken(name),
fetchCodexModels(name).catch((err: unknown) => {
// Empty catalog falls back to CODEX_DEFAULT_MODELS below; log so
// rate-limit/network failures are visible while switching profiles.
logger.debug("Codex models fetch failed for profile {profile}; using defaults: {error}", {
profile: name,
error: err instanceof Error ? err.message : String(err),
});
return [];
}),
]).then(
([token, liveModels]) => {
const accountId = token.accountId;
// Prefer the account's live model catalog; fall back to the current
Expand Down
Loading
Loading