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:
@@ -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} 轮`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user