Claude CLI under CLAUDE_CODE_OAUTH_TOKEN exits 1 without setting `is_error` when the OAuth subscription's quota is exhausted. The existing fallback chain (`lastResultError || stderr || tailLines(stdout)`) had nothing structured to grab and dumped ~2KB of `system/init` NDJSON into the progress comment, hiding the actionable quota notice the CLI had already printed as plain text. Capture non-JSON stdout lines into a 20-line ring buffer (mirroring the existing `recentStderr` pattern) and prefer it over the raw NDJSON tail. Generic — no regex on bubble text — so any human-readable line the CLI emits surfaces instead of the event stream. Also adds a `failure:claude-oauth-quota` bucket to `analyze-logs.ts`, ordered before the SIGTERM check so the NDJSON tail's `cancelled` / `cancel_url` substrings (from learnings content) stop shadowing it.
This commit is contained in:
committed by
pullfrog[bot]
parent
d857e06731
commit
56793d4a81
@@ -549,6 +549,13 @@ export async function runClaude(params: RunParams): Promise<ClaudeRunResult> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const recentStderr: string[] = [];
|
const recentStderr: string[] = [];
|
||||||
|
// ring buffer of recent non-JSON stdout lines. Claude CLI prints
|
||||||
|
// human-readable TTY chrome (status bubbles, quota notices, etc.)
|
||||||
|
// alongside the NDJSON event stream. when the CLI exits non-zero without
|
||||||
|
// emitting a structured error event, these lines are the only actionable
|
||||||
|
// signal — preferring them over the NDJSON tail keeps progress comments
|
||||||
|
// readable. issue #643.
|
||||||
|
const recentNonJsonStdout: string[] = [];
|
||||||
|
|
||||||
let lastProviderError: string | null = null;
|
let lastProviderError: string | null = null;
|
||||||
|
|
||||||
@@ -594,6 +601,8 @@ export async function runClaude(params: RunParams): Promise<ClaudeRunResult> {
|
|||||||
event = JSON.parse(trimmed) as ClaudeEvent;
|
event = JSON.parse(trimmed) as ClaudeEvent;
|
||||||
} catch {
|
} catch {
|
||||||
log.debug(`» non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
log.debug(`» non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
||||||
|
recentNonJsonStdout.push(trimmed);
|
||||||
|
if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,9 +704,16 @@ export async function runClaude(params: RunParams): Promise<ClaudeRunResult> {
|
|||||||
const stdoutSnapshot = output.toString();
|
const stdoutSnapshot = output.toString();
|
||||||
const stderrSnapshot = recentStderr.join("\n");
|
const stderrSnapshot = recentStderr.join("\n");
|
||||||
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
||||||
|
// prefer non-JSON stdout (human-readable TTY chrome the CLI prints,
|
||||||
|
// including status bubbles and quota notices) over the raw NDJSON
|
||||||
|
// tail. when the CLI exits 1 without emitting `is_error` (issue #643),
|
||||||
|
// the NDJSON fallback would otherwise dump 2KB of `system/init` events
|
||||||
|
// into the progress comment with no mention of the actual cause.
|
||||||
|
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
lastResultError ||
|
lastResultError ||
|
||||||
stderrSnapshot ||
|
stderrSnapshot ||
|
||||||
|
nonJsonStdoutSnapshot ||
|
||||||
truncatedStdout ||
|
truncatedStdout ||
|
||||||
`unknown error - no output from Claude CLI${errorContext}`;
|
`unknown error - no output from Claude CLI${errorContext}`;
|
||||||
log.error(
|
log.error(
|
||||||
|
|||||||
Reference in New Issue
Block a user