fix: accumulate compaction counts across retries

This commit is contained in:
Josh Lehman 2026-03-14 07:54:21 -07:00
parent 37313844c1
commit df8f292039
No known key found for this signature in database
GPG Key ID: D141B425AC7F876B
2 changed files with 127 additions and 147 deletions

View File

@ -319,59 +319,79 @@ export async function runAgentTurnWithFallback(params: {
}, },
); );
return (async () => { return (async () => {
const result = await runEmbeddedPiAgent({ let attemptCompactionCount = 0;
...embeddedContext, try {
trigger: params.isHeartbeat ? "heartbeat" : "user", const result = await runEmbeddedPiAgent({
groupId: resolveGroupSessionKey(params.sessionCtx)?.id, ...embeddedContext,
groupChannel: trigger: params.isHeartbeat ? "heartbeat" : "user",
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(), groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined, groupChannel:
...senderContext, params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
...runBaseParams, groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
prompt: params.commandBody, ...senderContext,
extraSystemPrompt: params.followupRun.run.extraSystemPrompt, ...runBaseParams,
toolResultFormat: (() => { prompt: params.commandBody,
const channel = resolveMessageChannel( extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
params.sessionCtx.Surface, toolResultFormat: (() => {
params.sessionCtx.Provider, const channel = resolveMessageChannel(
); params.sessionCtx.Surface,
if (!channel) { params.sessionCtx.Provider,
return "markdown"; );
} if (!channel) {
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain"; return "markdown";
})(), }
suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings, return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
bootstrapContextMode: params.opts?.bootstrapContextMode, })(),
bootstrapContextRunKind: params.opts?.isHeartbeat ? "heartbeat" : "default", suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings,
images: params.opts?.images, bootstrapContextMode: params.opts?.bootstrapContextMode,
abortSignal: params.opts?.abortSignal, bootstrapContextRunKind: params.opts?.isHeartbeat ? "heartbeat" : "default",
blockReplyBreak: params.resolvedBlockStreamingBreak, images: params.opts?.images,
blockReplyChunking: params.blockReplyChunking, abortSignal: params.opts?.abortSignal,
onPartialReply: async (payload) => { blockReplyBreak: params.resolvedBlockStreamingBreak,
const textForTyping = await handlePartialForTyping(payload); blockReplyChunking: params.blockReplyChunking,
if (!params.opts?.onPartialReply || textForTyping === undefined) { onPartialReply: async (payload) => {
return; const textForTyping = await handlePartialForTyping(payload);
} if (!params.opts?.onPartialReply || textForTyping === undefined) {
await params.opts.onPartialReply({ return;
text: textForTyping, }
mediaUrls: payload.mediaUrls, await params.opts.onPartialReply({
}); text: textForTyping,
}, mediaUrls: payload.mediaUrls,
onAssistantMessageStart: async () => { });
await params.typingSignals.signalMessageStart(); },
await params.opts?.onAssistantMessageStart?.(); onAssistantMessageStart: async () => {
}, await params.typingSignals.signalMessageStart();
onReasoningStream: await params.opts?.onAssistantMessageStart?.();
params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream },
? async (payload) => { onReasoningStream:
await params.typingSignals.signalReasoningDelta(); params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream
await params.opts?.onReasoningStream?.({ ? async (payload) => {
text: payload.text, await params.typingSignals.signalReasoningDelta();
mediaUrls: payload.mediaUrls, await params.opts?.onReasoningStream?.({
}); text: payload.text,
mediaUrls: payload.mediaUrls,
});
}
: undefined,
onReasoningEnd: params.opts?.onReasoningEnd,
onAgentEvent: async (evt) => {
// Signal run start only after the embedded agent emits real activity.
const hasLifecyclePhase =
evt.stream === "lifecycle" && typeof evt.data.phase === "string";
if (evt.stream !== "lifecycle" || hasLifecyclePhase) {
notifyAgentRunStart();
}
// Trigger typing when tools start executing.
// Must await to ensure typing indicator starts before tool summaries are emitted.
if (evt.stream === "tool") {
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
const name = typeof evt.data.name === "string" ? evt.data.name : undefined;
if (phase === "start" || phase === "update") {
await params.typingSignals.signalToolStart();
await params.opts?.onToolStart?.({ name, phase });
} }
} }
// Track auto-compaction completion and notify UI layer // Track auto-compaction completion and notify UI layer.
if (evt.stream === "compaction") { if (evt.stream === "compaction") {
const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
if (phase === "start") { if (phase === "start") {
@ -401,104 +421,63 @@ export async function runAgentTurnWithFallback(params: {
directlySentBlockKeys, directlySentBlockKeys,
}) })
: undefined, : undefined,
onReasoningEnd: params.opts?.onReasoningEnd, onBlockReplyFlush:
onAgentEvent: async (evt) => { params.blockStreamingEnabled && blockReplyPipeline
// Signal run start only after the embedded agent emits real activity. ? async () => {
const hasLifecyclePhase = await blockReplyPipeline.flush({ force: true });
evt.stream === "lifecycle" && typeof evt.data.phase === "string"; }
if (evt.stream !== "lifecycle" || hasLifecyclePhase) { : undefined,
notifyAgentRunStart(); shouldEmitToolResult: params.shouldEmitToolResult,
} shouldEmitToolOutput: params.shouldEmitToolOutput,
// Trigger typing when tools start executing. bootstrapPromptWarningSignaturesSeen,
// Must await to ensure typing indicator starts before tool summaries are emitted. bootstrapPromptWarningSignature:
if (evt.stream === "tool") { bootstrapPromptWarningSignaturesSeen[
const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; bootstrapPromptWarningSignaturesSeen.length - 1
const name = typeof evt.data.name === "string" ? evt.data.name : undefined; ],
if (phase === "start" || phase === "update") { onToolResult: onToolResult
await params.typingSignals.signalToolStart(); ? (() => {
await params.opts?.onToolStart?.({ name, phase }); // Serialize tool result delivery to preserve message ordering.
} // Without this, concurrent tool callbacks race through typing signals
} // and message sends, causing out-of-order delivery to the user.
// Track auto-compaction completion and notify UI layer // See: https://github.com/openclaw/openclaw/issues/11044
if (evt.stream === "compaction") { let toolResultChain: Promise<void> = Promise.resolve();
const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; return (payload: ReplyPayload) => {
if (phase === "start") { toolResultChain = toolResultChain
await params.opts?.onCompactionStart?.(); .then(async () => {
} const { text, skip } = normalizeStreamingText(payload);
if (phase === "end") { if (skip) {
autoCompactionCount += 1; return;
await params.opts?.onCompactionEnd?.(); }
} await params.typingSignals.signalTextDelta(text);
} await onToolResult({
}, ...payload,
// Always pass onBlockReply so flushBlockReplyBuffer works before tool execution, text,
// even when regular block streaming is disabled. The handler sends directly });
// via opts.onBlockReply when the pipeline isn't available. })
onBlockReply: params.opts?.onBlockReply .catch((err) => {
? createBlockReplyDeliveryHandler({ // Keep chain healthy after an error so later tool results still deliver.
onBlockReply: params.opts.onBlockReply, logVerbose(`tool result delivery failed: ${String(err)}`);
currentMessageId:
params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid,
normalizeStreamingText,
applyReplyToMode: params.applyReplyToMode,
normalizeMediaPaths: normalizeReplyMediaPaths,
typingSignals: params.typingSignals,
blockStreamingEnabled: params.blockStreamingEnabled,
blockReplyPipeline,
directlySentBlockKeys,
})
: undefined,
onBlockReplyFlush:
params.blockStreamingEnabled && blockReplyPipeline
? async () => {
await blockReplyPipeline.flush({ force: true });
}
: undefined,
shouldEmitToolResult: params.shouldEmitToolResult,
shouldEmitToolOutput: params.shouldEmitToolOutput,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature:
bootstrapPromptWarningSignaturesSeen[
bootstrapPromptWarningSignaturesSeen.length - 1
],
onToolResult: onToolResult
? (() => {
// Serialize tool result delivery to preserve message ordering.
// Without this, concurrent tool callbacks race through typing signals
// and message sends, causing out-of-order delivery to the user.
// See: https://github.com/openclaw/openclaw/issues/11044
let toolResultChain: Promise<void> = Promise.resolve();
return (payload: ReplyPayload) => {
toolResultChain = toolResultChain
.then(async () => {
const { text, skip } = normalizeStreamingText(payload);
if (skip) {
return;
}
await params.typingSignals.signalTextDelta(text);
await onToolResult({
...payload,
text,
}); });
}) const task = toolResultChain.finally(() => {
.catch((err) => { params.pendingToolTasks.delete(task);
// Keep chain healthy after an error so later tool results still deliver.
logVerbose(`tool result delivery failed: ${String(err)}`);
}); });
const task = toolResultChain.finally(() => { params.pendingToolTasks.add(task);
params.pendingToolTasks.delete(task); };
}); })()
params.pendingToolTasks.add(task); : undefined,
}; });
})() bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
: undefined, result.meta?.systemPromptReport,
}); );
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen( const resultCompactionCount = Math.max(
result.meta?.systemPromptReport, 0,
); result.meta?.agentMeta?.compactionCount ?? 0,
const resultCompactionCount = Math.max(0, result.meta?.agentMeta?.compactionCount ?? 0); );
autoCompactionCount = Math.max(autoCompactionCount, resultCompactionCount); attemptCompactionCount = Math.max(attemptCompactionCount, resultCompactionCount);
return result; return result;
} finally {
autoCompactionCount += attemptCompactionCount;
}
})(); })();
}, },
}); });

View File

@ -168,6 +168,7 @@ export function createFollowupRunner(params: {
}), }),
run: async (provider, model, runOptions) => { run: async (provider, model, runOptions) => {
const authProfile = resolveRunAuthProfile(queued.run, provider); const authProfile = resolveRunAuthProfile(queued.run, provider);
let attemptCompactionCount = 0;
try { try {
const result = await runEmbeddedPiAgent({ const result = await runEmbeddedPiAgent({
sessionId: queued.run.sessionId, sessionId: queued.run.sessionId,