Show the error reason after the generic failure message
When an agent turn fails, users only saw "요청을 완료하지 못했습니다. 다시
시도해 주세요." with no clue why. Capture the failure reason (explicit
error field, or provider-error text like "API Error: 529 Overloaded"
classified by detectClaudeProviderFailureMessage) in the turn controller
and append it to the failure message ("...\n\n오류 내용: <reason>",
secret-redacted + length-capped). Update the paired-room loop filter to
startsWith so a failure-with-reason is still recognized and not
re-injected into prompt history. Unit-tested.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
getAgentOutputAttachments,
|
||||
getAgentOutputText,
|
||||
} from './agent-output.js';
|
||||
import { detectClaudeProviderFailureMessage } from './agent-error-detection.js';
|
||||
import { createScopedLogger, logger } from './logger.js';
|
||||
import { sanitizeForOutbound } from './router.js';
|
||||
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
|
||||
@@ -23,6 +24,25 @@ import {
|
||||
|
||||
export type { VisiblePhase };
|
||||
|
||||
/**
|
||||
* Append a concise error reason to the generic failure message so the user can
|
||||
* see, at a glance, WHY the turn failed (e.g. an Anthropic "529 Overloaded").
|
||||
* The detail is secret-redacted + whitespace-collapsed + length-capped. Returns
|
||||
* the base text unchanged when there is no usable detail.
|
||||
*/
|
||||
export function appendErrorDetailToFailureText(
|
||||
base: string,
|
||||
detail: string | null | undefined,
|
||||
): string {
|
||||
const cleaned = detail
|
||||
? sanitizeForOutbound(detail).replace(/\s+/g, ' ').trim()
|
||||
: '';
|
||||
if (!cleaned) return base;
|
||||
const MAX = 300;
|
||||
const shown = cleaned.length > MAX ? `${cleaned.slice(0, MAX)}…` : cleaned;
|
||||
return `${base}\n\n오류 내용: ${shown}`;
|
||||
}
|
||||
|
||||
interface SubagentTrack {
|
||||
label: string;
|
||||
activities: string[];
|
||||
@@ -59,6 +79,7 @@ export class MessageTurnController {
|
||||
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private visiblePhase: VisiblePhase = 'silent';
|
||||
private hadError = false;
|
||||
private lastErrorDetail: string | null = null;
|
||||
private producedDeliverySucceeded = true;
|
||||
private latestProgressText: string | null = null;
|
||||
private previousProgressText: string | null = null;
|
||||
@@ -175,6 +196,19 @@ export class MessageTurnController {
|
||||
const text = raw ? sanitizeForOutbound(raw) : null;
|
||||
const attachments = getAgentOutputAttachments(result);
|
||||
|
||||
// Remember the most recent failure REASON so the generic failure message
|
||||
// can tell the user WHAT went wrong (e.g. "API Error: 529 Overloaded").
|
||||
// Only capture a genuine error reason — an explicit `error` field, or text
|
||||
// the classifier recognizes as a provider error (429 / overloaded / network)
|
||||
// that the SDK surfaces as a success-typed result. Do NOT capture arbitrary
|
||||
// progress text that merely carries an error status.
|
||||
const explicitError = result.error?.trim();
|
||||
if (explicitError) {
|
||||
this.lastErrorDetail = explicitError;
|
||||
} else if (raw && detectClaudeProviderFailureMessage(raw)) {
|
||||
this.lastErrorDetail = raw.trim();
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
this.log.info(
|
||||
{
|
||||
@@ -820,7 +854,12 @@ export class MessageTurnController {
|
||||
if (this.terminalObserved()) {
|
||||
return;
|
||||
}
|
||||
await this.publishTerminalText(this.options.failureFinalText);
|
||||
await this.publishTerminalText(
|
||||
appendErrorDetailToFailureText(
|
||||
this.options.failureFinalText,
|
||||
this.lastErrorDetail,
|
||||
),
|
||||
);
|
||||
}
|
||||
private requestAgentClose(reason: string): void {
|
||||
if (this.closeRequested) return;
|
||||
|
||||
Reference in New Issue
Block a user