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:
Yaowei Zheng
2026-07-29 16:57:05 +08:00
committed by GitHub
parent 8dc560eedd
commit 5b22ad06da
30 changed files with 603 additions and 174 deletions
+16 -6
View File
@@ -372,7 +372,7 @@ export class TraceService {
if (pendingFrom === null) pendingFrom = mi;
// A user Prompt **always starts a new turn**: judging continuation solely
// by "did the previous turn call a tool" isn't enough — if the previous
// turn ended in timeout/malformed (given up after exhausting retries),
// turn ended in a retryable status (given up after exhausting retries),
// retryable would leave continuation at true, and this new message would
// get merged into that failed turn, smearing the two turns' messages /
// Tokens / TPS / duration together.
@@ -437,11 +437,21 @@ export class TraceService {
}
} else if (p.type === "request_end") {
const status = typeof p.status === "string" ? p.status : undefined;
// timeout/malformed is automatically reconnected by core within the same
// run (context-engine's retry loop); the resent Request still belongs to
// **the same user turn**: it must continue the turn, otherwise a single
// timeout would split that turn's Tokens/duration/TPS across two Tasks.
const retryable = status === "timeout" || status === "malformed";
// A status core reconnects on within the same run (context-engine's retry
// loop) leaves the resent Request in **the same user turn**: it must
// continue the turn, otherwise a single blip would split that turn's
// Tokens/duration/TPS across two Tasks and inflate the Task count.
//
// This list must track the engine's, and the engine's differs by loop:
// the turn loop retries `failed` too, while compaction deliberately fails
// fast and stops on it (core's TURN_RETRY_STATUSES vs
// COMPACTION_RETRY_STATUSES). Hence the compactionActive guard — a failed
// compaction request really is the end of that request, and counting it
// as a reconnect would invent an attempt that never happened.
const retryable =
status === "timeout" ||
status === "malformed" ||
(status === "failed" && !compactionActive);
if (!hasOrigin) {
prevSerialTs = null;
continuation = sawToolCallThisRequest || retryable;