fix(core): retry on failed as well as timeout; only auth stops the run (#104)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -153,8 +153,12 @@ export interface Messages {
|
||||
}): string;
|
||||
/** Abort event label (may include a reason). */
|
||||
abortLabel(reason?: string): string;
|
||||
/** request_end ended with timeout/malformed: the engine retries (reconnect) carrying already-produced content; attempt is the retry count. */
|
||||
reconnectLabel(status: "timeout" | "malformed", attempt: number): string;
|
||||
/**
|
||||
* request_end ended with a status the engine reconnects on (`failed` / `timeout` /
|
||||
* `malformed` — only `auth` is terminal): the engine retries carrying already-produced
|
||||
* content; attempt is the retry count.
|
||||
*/
|
||||
reconnectLabel(status: "failed" | "timeout" | "malformed", attempt: number): string;
|
||||
/** compaction start event: indicates compaction in progress (mode is summarize/discard, reason is context/turns/manual). */
|
||||
compactionStart(mode: string, reason: string): string;
|
||||
/**
|
||||
@@ -383,7 +387,13 @@ const en: Messages = {
|
||||
`[stats] context ${s.context} (${s.contextDelta}) · tokens ${s.tokens} (${s.tokensDelta}) · ${s.elapsed} (${s.elapsedDelta})`,
|
||||
abortLabel: (reason) => `[abort]${reason ? `: ${reason}` : ""}`,
|
||||
reconnectLabel: (status, attempt) =>
|
||||
`[retry] ${status === "timeout" ? "connection timed out" : "response incomplete or unparseable"}; sending retry #${attempt}…`,
|
||||
`[retry] ${
|
||||
status === "timeout"
|
||||
? "connection timed out"
|
||||
: status === "malformed"
|
||||
? "response incomplete or unparseable"
|
||||
: "the model provider returned an error"
|
||||
}; sending retry #${attempt}…`,
|
||||
compactionStart: (mode, reason) =>
|
||||
mode === "discard"
|
||||
? `[compaction] discarding context (${reason})…`
|
||||
@@ -571,7 +581,13 @@ const zh: Messages = {
|
||||
`[统计信息] 上下文 ${s.context} (${s.contextDelta}) · tokens ${s.tokens} (${s.tokensDelta}) · 用时 ${s.elapsed} (${s.elapsedDelta})`,
|
||||
abortLabel: (reason) => `[已中断]${reason ? `:${reason}` : ""}`,
|
||||
reconnectLabel: (status, attempt) =>
|
||||
`[重试] ${status === "timeout" ? "连接超时或网络中断" : "响应不完整或无法解析"},正在发起第 ${attempt} 次重试……`,
|
||||
`[重试] ${
|
||||
status === "timeout"
|
||||
? "连接超时或网络中断"
|
||||
: status === "malformed"
|
||||
? "响应不完整或无法解析"
|
||||
: "模型服务返回错误"
|
||||
},正在发起第 ${attempt} 次重试……`,
|
||||
compactionStart: (mode, reason) =>
|
||||
mode === "discard"
|
||||
? `[压缩] 正在丢弃旧上下文(${reason})……`
|
||||
|
||||
@@ -378,8 +378,8 @@ export class StreamRenderer {
|
||||
*/
|
||||
private taskFirstTsMs: number | null = null;
|
||||
private taskLastReqEndMs: number | null = null;
|
||||
/** Terminal state (timeout/malformed) of the previous request: the next request_begin is a retry, at which point a notice is printed. */
|
||||
private pendingRetry: "timeout" | "malformed" | null = null;
|
||||
/** Retryable terminal state (failed/timeout/malformed) of the previous request: the next request_begin is a retry, at which point a notice is printed. */
|
||||
private pendingRetry: "failed" | "timeout" | "malformed" | null = null;
|
||||
/** Number of retries already initiated (increments on consecutive failures, reset once a request completes normally). */
|
||||
private reconnectRun = 0;
|
||||
|
||||
@@ -693,7 +693,7 @@ export class StreamRenderer {
|
||||
this.out.write(`${formatAbort(payload as AbortPayload, this.t)}\n`);
|
||||
this.lastLineKey = null;
|
||||
} else if (payload.type === "request_begin") {
|
||||
// The previous request ended in timeout/malformed -> this request is a retry
|
||||
// The previous request ended in a retryable status -> this request is a retry
|
||||
// carrying [turn_retried]: printed when the retry **actually starts** (when
|
||||
// retries are exhausted, there's no retry after the last failure, only an abort
|
||||
// explaining why).
|
||||
@@ -720,7 +720,10 @@ export class StreamRenderer {
|
||||
this.hasUsage = true;
|
||||
}
|
||||
}
|
||||
if (p.status === "timeout" || p.status === "malformed") {
|
||||
// Every status the engine reconnects on, `failed` included — only `auth` is terminal.
|
||||
// Leaving `failed` out would print nothing for a retry that is really happening, and
|
||||
// reset the counter mid-ladder so a mixed run renumbers back to retry #1.
|
||||
if (p.status === "failed" || p.status === "timeout" || p.status === "malformed") {
|
||||
this.pendingRetry = p.status;
|
||||
} else {
|
||||
this.pendingRetry = null;
|
||||
|
||||
Reference in New Issue
Block a user