fix(core,server,web,cli): compaction tolerance, retry parity, and failure classification (#174)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-04 17:27:45 +08:00
committed by GitHub
parent 7015fb0153
commit 6f60b242bd
32 changed files with 741 additions and 326 deletions
+10 -5
View File
@@ -171,7 +171,12 @@ export interface Messages {
* line: total = Session cumulative, delta = consumed by this compaction, carrying its own
* sign); when present it is appended at the end of the line, e.g. ` · tokens 14k (+6k)`.
*/
compactionStop(mode: string, status: string, tokens?: { total: string; delta: string }): string;
compactionStop(
mode: string,
status: string,
tokens?: { total: string; delta: string },
errorMessage?: string,
): string;
/** Prompt shown when `/compact` has nothing to compact (session just started / two consecutive compactions). */
compactNothing(): string;
/** Dim line announcing one goal round (printed before the round runs). */
@@ -406,12 +411,12 @@ const en: Messages = {
mode === "discard"
? `[compaction] discarding context (${reason})…`
: `[compaction] summarizing context (${reason})…`,
compactionStop: (mode, status, tokens) =>
compactionStop: (mode, status, tokens, errorMessage) =>
(status === "completed"
? mode === "discard"
? "[compaction] done; old context discarded"
: "[compaction] done; continuing with the summarized context"
: `[compaction] ${status}; keeping the current context`) +
: `[compaction] ${status}${errorMessage !== undefined ? ` (${errorMessage})` : ""}; keeping the current context`) +
(tokens ? ` · tokens ${tokens.total} (${tokens.delta})` : ""),
compactNothing: () => "[compaction] nothing to compact yet",
goalRound: (round) => `[goal] round ${round}`,
@@ -620,12 +625,12 @@ const zh: Messages = {
mode === "discard"
? `[压缩] 正在丢弃旧上下文(${reason})……`
: `[压缩] 正在总结压缩上下文(${reason})……`,
compactionStop: (mode, status, tokens) =>
compactionStop: (mode, status, tokens, errorMessage) =>
(status === "completed"
? mode === "discard"
? "[压缩] 完成,旧上下文已丢弃"
: "[压缩] 完成,已切换到摘要后的新上下文"
: `[压缩] ${status === "aborted" ? "已中断" : "失败"},保留当前上下文`) +
: `[压缩] ${status === "aborted" ? "已中断" : `失败${errorMessage !== undefined ? `(${errorMessage})` : ""}`},保留当前上下文`) +
(tokens ? ` · tokens ${tokens.total} (${tokens.delta})` : ""),
compactNothing: () => "[压缩] 当前上下文为空,无需压缩",
goalRound: (round) => `[目标] 第 ${round} 轮`,
+13 -7
View File
@@ -380,8 +380,8 @@ export class StreamRenderer {
private taskLastReqEndMs: number | 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;
/** The pending failure's attempt ordinal (request_end.attempt — the core's authoritative count, printed on the retry line). */
private pendingRetryAttempt: number | undefined;
constructor(out: NodeJS.WritableStream = process.stdout, t: Messages = defaultMessages()) {
this.out = out;
@@ -688,7 +688,7 @@ export class StreamRenderer {
} else if (payload.type === "abort") {
// Run ended (user interrupt / retries exhausted): clear any pending retry state so the next run doesn't mistakenly print a retry line.
this.pendingRetry = null;
this.reconnectRun = 0;
this.pendingRetryAttempt = undefined;
this.finishLine();
this.out.write(`${formatAbort(payload as AbortPayload, this.t)}\n`);
this.lastLineKey = null;
@@ -698,9 +698,12 @@ export class StreamRenderer {
// retries are exhausted, there's no retry after the last failure, only an abort
// explaining why).
if (this.pendingRetry) {
this.reconnectRun += 1;
// The failed request_end stamped its authoritative attempt ordinal — print it
// (the CLI renders live streams only, which always come from a stamping core).
const attempt = this.pendingRetryAttempt ?? 1;
this.pendingRetryAttempt = undefined;
this.finishLine();
this.out.write(`${dim(this.t.reconnectLabel(this.pendingRetry, this.reconnectRun))}\n`);
this.out.write(`${dim(this.t.reconnectLabel(this.pendingRetry, attempt))}\n`);
this.lastLineKey = null;
this.pendingRetry = null;
}
@@ -725,9 +728,10 @@ export class StreamRenderer {
// 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;
this.pendingRetryAttempt = p.attempt;
} else {
this.pendingRetry = null;
this.reconnectRun = 0;
this.pendingRetryAttempt = undefined;
}
} else if (payload.type === "compaction_begin") {
// Paired compaction events: begin signals compaction is in progress.
@@ -751,7 +755,9 @@ export class StreamRenderer {
}
: undefined;
this.compactionTokens = 0;
this.out.write(`${dim(this.t.compactionStop(p.mode, p.status, tokens))}\n`);
this.out.write(
`${dim(this.t.compactionStop(p.mode, p.status, tokens, p.error_message))}\n`,
);
this.lastLineKey = null;
}
return;