Change recovery phase ordering to DCP → Truncate → Summarize
When session hits token limit (e.g. 207k > 200k), the summarize API also fails because it needs to process the full 207k tokens. By truncating FIRST, we reduce token count before attempting summarize. Changes: - PHASE 1: DCP (Dynamic Context Pruning) - prune duplicate tool calls first - PHASE 2: Aggressive Truncation - always try when over limit - PHASE 3: Summarize - last resort after DCP and truncation 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -295,17 +295,16 @@ export async function executeCompact(
|
|||||||
const errorData = autoCompactState.errorDataBySession.get(sessionID);
|
const errorData = autoCompactState.errorDataBySession.get(sessionID);
|
||||||
const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
|
const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
|
||||||
|
|
||||||
// DCP FIRST - run before any other recovery attempts when token limit exceeded (controlled by dcp-for-compaction hook)
|
const isOverLimit =
|
||||||
const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
|
|
||||||
if (
|
|
||||||
dcpForCompaction !== false &&
|
|
||||||
!dcpState.attempted &&
|
|
||||||
errorData?.currentTokens &&
|
errorData?.currentTokens &&
|
||||||
errorData?.maxTokens &&
|
errorData?.maxTokens &&
|
||||||
errorData.currentTokens > errorData.maxTokens
|
errorData.currentTokens > errorData.maxTokens;
|
||||||
) {
|
|
||||||
|
// PHASE 1: DCP (Dynamic Context Pruning) - prune duplicate tool calls first
|
||||||
|
const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
|
||||||
|
if (dcpForCompaction !== false && !dcpState.attempted && isOverLimit) {
|
||||||
dcpState.attempted = true;
|
dcpState.attempted = true;
|
||||||
log("[auto-compact] DCP triggered FIRST on token limit error", {
|
log("[auto-compact] PHASE 1: DCP triggered on token limit error", {
|
||||||
sessionID,
|
sessionID,
|
||||||
currentTokens: errorData.currentTokens,
|
currentTokens: errorData.currentTokens,
|
||||||
maxTokens: errorData.maxTokens,
|
maxTokens: errorData.maxTokens,
|
||||||
@@ -314,19 +313,25 @@ export async function executeCompact(
|
|||||||
const dcpConfig = experimental?.dynamic_context_pruning ?? {
|
const dcpConfig = experimental?.dynamic_context_pruning ?? {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
notification: "detailed" as const,
|
notification: "detailed" as const,
|
||||||
protected_tools: ["task", "todowrite", "todoread", "lsp_rename", "lsp_code_action_resolve"],
|
protected_tools: [
|
||||||
|
"task",
|
||||||
|
"todowrite",
|
||||||
|
"todoread",
|
||||||
|
"lsp_rename",
|
||||||
|
"lsp_code_action_resolve",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pruningResult = await executeDynamicContextPruning(
|
const pruningResult = await executeDynamicContextPruning(
|
||||||
sessionID,
|
sessionID,
|
||||||
dcpConfig,
|
dcpConfig,
|
||||||
client
|
client,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pruningResult.itemsPruned > 0) {
|
if (pruningResult.itemsPruned > 0) {
|
||||||
dcpState.itemsPruned = pruningResult.itemsPruned;
|
dcpState.itemsPruned = pruningResult.itemsPruned;
|
||||||
log("[auto-compact] DCP successful, proceeding to compaction", {
|
log("[auto-compact] DCP successful, proceeding to truncation", {
|
||||||
itemsPruned: pruningResult.itemsPruned,
|
itemsPruned: pruningResult.itemsPruned,
|
||||||
tokensSaved: pruningResult.totalTokensSaved,
|
tokensSaved: pruningResult.totalTokensSaved,
|
||||||
});
|
});
|
||||||
@@ -335,56 +340,13 @@ export async function executeCompact(
|
|||||||
.showToast({
|
.showToast({
|
||||||
body: {
|
body: {
|
||||||
title: "Dynamic Context Pruning",
|
title: "Dynamic Context Pruning",
|
||||||
message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Running compaction...`,
|
message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Proceeding to truncation...`,
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
// Continue to PHASE 2 (truncation) instead of summarizing immediately
|
||||||
// After DCP, immediately try summarize
|
|
||||||
const providerID = msg.providerID as string | undefined;
|
|
||||||
const modelID = msg.modelID as string | undefined;
|
|
||||||
|
|
||||||
if (providerID && modelID) {
|
|
||||||
try {
|
|
||||||
sanitizeEmptyMessagesBeforeSummarize(sessionID);
|
|
||||||
|
|
||||||
await (client as Client).tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Auto Compact",
|
|
||||||
message: "Summarizing session after DCP...",
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
await (client as Client).session.summarize({
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: { providerID, modelID },
|
|
||||||
query: { directory },
|
|
||||||
});
|
|
||||||
|
|
||||||
clearSessionState(autoCompactState, sessionID);
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await (client as Client).session.prompt_async({
|
|
||||||
path: { sessionID },
|
|
||||||
body: { parts: [{ type: "text", text: "Continue" }] },
|
|
||||||
query: { directory },
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
} catch (summarizeError) {
|
|
||||||
log("[auto-compact] summarize after DCP failed, continuing recovery", {
|
|
||||||
error: String(summarizeError),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log("[auto-compact] DCP did not prune any items", { sessionID });
|
log("[auto-compact] DCP did not prune any items", { sessionID });
|
||||||
}
|
}
|
||||||
@@ -393,14 +355,12 @@ export async function executeCompact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PHASE 2: Aggressive Truncation - always try when over limit (not experimental-only)
|
||||||
if (
|
if (
|
||||||
experimental?.aggressive_truncation &&
|
isOverLimit &&
|
||||||
errorData?.currentTokens &&
|
|
||||||
errorData?.maxTokens &&
|
|
||||||
errorData.currentTokens > errorData.maxTokens &&
|
|
||||||
truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts
|
truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts
|
||||||
) {
|
) {
|
||||||
log("[auto-compact] aggressive truncation triggered (experimental)", {
|
log("[auto-compact] PHASE 2: aggressive truncation triggered", {
|
||||||
currentTokens: errorData.currentTokens,
|
currentTokens: errorData.currentTokens,
|
||||||
maxTokens: errorData.maxTokens,
|
maxTokens: errorData.maxTokens,
|
||||||
targetRatio: TRUNCATE_CONFIG.targetTokenRatio,
|
targetRatio: TRUNCATE_CONFIG.targetTokenRatio,
|
||||||
@@ -422,16 +382,16 @@ export async function executeCompact(
|
|||||||
.join(", ");
|
.join(", ");
|
||||||
const statusMsg = aggressiveResult.sufficient
|
const statusMsg = aggressiveResult.sufficient
|
||||||
? `Truncated ${aggressiveResult.truncatedCount} outputs (${formatBytes(aggressiveResult.totalBytesRemoved)})`
|
? `Truncated ${aggressiveResult.truncatedCount} outputs (${formatBytes(aggressiveResult.totalBytesRemoved)})`
|
||||||
: `Truncated ${aggressiveResult.truncatedCount} outputs (${formatBytes(aggressiveResult.totalBytesRemoved)}) but need ${formatBytes(aggressiveResult.targetBytesToRemove)}. Falling back to summarize/revert...`;
|
: `Truncated ${aggressiveResult.truncatedCount} outputs (${formatBytes(aggressiveResult.totalBytesRemoved)}) - continuing to summarize...`;
|
||||||
|
|
||||||
await (client as Client).tui
|
await (client as Client).tui
|
||||||
.showToast({
|
.showToast({
|
||||||
body: {
|
body: {
|
||||||
title: aggressiveResult.sufficient
|
title: aggressiveResult.sufficient
|
||||||
? "Aggressive Truncation"
|
? "Truncation Complete"
|
||||||
: "Partial Truncation",
|
: "Partial Truncation",
|
||||||
message: `${statusMsg}: ${toolNames}`,
|
message: `${statusMsg}: ${toolNames}`,
|
||||||
variant: "warning",
|
variant: aggressiveResult.sufficient ? "success" : "warning",
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -439,7 +399,9 @@ export async function executeCompact(
|
|||||||
|
|
||||||
log("[auto-compact] aggressive truncation completed", aggressiveResult);
|
log("[auto-compact] aggressive truncation completed", aggressiveResult);
|
||||||
|
|
||||||
|
// If truncation was sufficient, try to continue without summarize
|
||||||
if (aggressiveResult.sufficient) {
|
if (aggressiveResult.sufficient) {
|
||||||
|
clearSessionState(autoCompactState, sessionID);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await (client as Client).session.prompt_async({
|
await (client as Client).session.prompt_async({
|
||||||
@@ -451,86 +413,13 @@ export async function executeCompact(
|
|||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If not sufficient, fall through to PHASE 3 (summarize)
|
||||||
} else {
|
} else {
|
||||||
await (client as Client).tui
|
log("[auto-compact] no tool outputs found to truncate", { sessionID });
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Truncation Skipped",
|
|
||||||
message: "No tool outputs found to truncate.",
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let skipSummarize = false;
|
// PHASE 3: Summarize - last resort after DCP and truncation
|
||||||
|
|
||||||
if (truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts) {
|
|
||||||
const largest = findLargestToolResult(sessionID);
|
|
||||||
|
|
||||||
if (
|
|
||||||
largest &&
|
|
||||||
largest.outputSize >= TRUNCATE_CONFIG.minOutputSizeToTruncate
|
|
||||||
) {
|
|
||||||
const result = truncateToolResult(largest.partPath);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
truncateState.truncateAttempt++;
|
|
||||||
truncateState.lastTruncatedPartId = largest.partId;
|
|
||||||
|
|
||||||
await (client as Client).tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Truncating Large Output",
|
|
||||||
message: `Truncated ${result.toolName} (${formatBytes(result.originalSize ?? 0)}). Retrying...`,
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await (client as Client).session.prompt_async({
|
|
||||||
path: { sessionID },
|
|
||||||
body: { parts: [{ type: "text", text: "Continue" }] },
|
|
||||||
query: { directory },
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
errorData?.currentTokens &&
|
|
||||||
errorData?.maxTokens &&
|
|
||||||
errorData.currentTokens > errorData.maxTokens
|
|
||||||
) {
|
|
||||||
skipSummarize = true;
|
|
||||||
await (client as Client).tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Summarize Skipped",
|
|
||||||
message: `Over token limit (${errorData.currentTokens}/${errorData.maxTokens}) with nothing to truncate. Going to revert...`,
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
} else if (!errorData?.currentTokens) {
|
|
||||||
await (client as Client).tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Truncation Skipped",
|
|
||||||
message: "No large tool outputs found.",
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryState = getOrCreateRetryState(autoCompactState, sessionID);
|
const retryState = getOrCreateRetryState(autoCompactState, sessionID);
|
||||||
|
|
||||||
@@ -581,7 +470,7 @@ export async function executeCompact(
|
|||||||
autoCompactState.truncateStateBySession.delete(sessionID);
|
autoCompactState.truncateStateBySession.delete(sessionID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipSummarize && retryState.attempt < RETRY_CONFIG.maxAttempts) {
|
if (retryState.attempt < RETRY_CONFIG.maxAttempts) {
|
||||||
retryState.attempt++;
|
retryState.attempt++;
|
||||||
retryState.lastAttemptTime = Date.now();
|
retryState.lastAttemptTime = Date.now();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user