fix(recovery): ignore empty summary-only assistant messages
This commit is contained in:
@@ -158,7 +158,7 @@ export async function getLastAssistant(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
client: any,
|
client: any,
|
||||||
directory: string,
|
directory: string,
|
||||||
): Promise<Record<string, unknown> | null> {
|
): Promise<{ info: Record<string, unknown>; hasContent: boolean } | null> {
|
||||||
try {
|
try {
|
||||||
const resp = await (client as Client).session.messages({
|
const resp = await (client as Client).session.messages({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
@@ -175,7 +175,15 @@ export async function getLastAssistant(
|
|||||||
return info?.role === "assistant"
|
return info?.role === "assistant"
|
||||||
})
|
})
|
||||||
if (!last) return null
|
if (!last) return null
|
||||||
return (last as { info?: Record<string, unknown> }).info ?? null
|
|
||||||
|
const message = last as SDKMessage & { info?: Record<string, unknown> }
|
||||||
|
const info = message.info
|
||||||
|
if (!info) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
info,
|
||||||
|
hasContent: messageHasContentFromSDK(message),
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import * as originalLogger from "../../shared/logger"
|
|||||||
|
|
||||||
const executeCompactMock = mock(async () => {})
|
const executeCompactMock = mock(async () => {})
|
||||||
const getLastAssistantMock = mock(async () => ({
|
const getLastAssistantMock = mock(async () => ({
|
||||||
providerID: "anthropic",
|
info: {
|
||||||
modelID: "claude-sonnet-4-6",
|
providerID: "anthropic",
|
||||||
|
modelID: "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
hasContent: true,
|
||||||
}))
|
}))
|
||||||
const parseAnthropicTokenLimitErrorMock = mock(() => ({
|
const parseAnthropicTokenLimitErrorMock = mock(() => ({
|
||||||
providerID: "anthropic",
|
providerID: "anthropic",
|
||||||
@@ -115,4 +118,43 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
restore()
|
restore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("does not treat empty summary assistant messages as successful compaction", async () => {
|
||||||
|
//#given
|
||||||
|
const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks()
|
||||||
|
getLastAssistantMock.mockResolvedValueOnce({
|
||||||
|
info: {
|
||||||
|
summary: true,
|
||||||
|
providerID: "anthropic",
|
||||||
|
modelID: "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
hasContent: false,
|
||||||
|
})
|
||||||
|
const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook")
|
||||||
|
const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext())
|
||||||
|
|
||||||
|
try {
|
||||||
|
//#when
|
||||||
|
await hook.event({
|
||||||
|
event: {
|
||||||
|
type: "session.error",
|
||||||
|
properties: { sessionID: "session-empty-summary", error: "prompt is too long" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await hook.event({
|
||||||
|
event: {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "session-empty-summary" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(getClearTimeoutCalls()).toEqual([1 as ReturnType<typeof setTimeout>])
|
||||||
|
expect(executeCompactMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-empty-summary")
|
||||||
|
} finally {
|
||||||
|
restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ export function createAnthropicContextWindowLimitRecoveryHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory)
|
const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory)
|
||||||
const providerID = parsed.providerID ?? (lastAssistant?.providerID as string | undefined)
|
const lastAssistantInfo = lastAssistant?.info
|
||||||
const modelID = parsed.modelID ?? (lastAssistant?.modelID as string | undefined)
|
const providerID = parsed.providerID ?? (lastAssistantInfo?.providerID as string | undefined)
|
||||||
|
const modelID = parsed.modelID ?? (lastAssistantInfo?.modelID as string | undefined)
|
||||||
|
|
||||||
await ctx.client.tui
|
await ctx.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
@@ -136,14 +137,15 @@ export function createAnthropicContextWindowLimitRecoveryHook(
|
|||||||
|
|
||||||
const errorData = autoCompactState.errorDataBySession.get(sessionID)
|
const errorData = autoCompactState.errorDataBySession.get(sessionID)
|
||||||
const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory)
|
const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory)
|
||||||
|
const lastAssistantInfo = lastAssistant?.info
|
||||||
|
|
||||||
if (lastAssistant?.summary === true) {
|
if (lastAssistantInfo?.summary === true && lastAssistant?.hasContent) {
|
||||||
autoCompactState.pendingCompact.delete(sessionID)
|
autoCompactState.pendingCompact.delete(sessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerID = errorData?.providerID ?? (lastAssistant?.providerID as string | undefined)
|
const providerID = errorData?.providerID ?? (lastAssistantInfo?.providerID as string | undefined)
|
||||||
const modelID = errorData?.modelID ?? (lastAssistant?.modelID as string | undefined)
|
const modelID = errorData?.modelID ?? (lastAssistantInfo?.modelID as string | undefined)
|
||||||
|
|
||||||
await ctx.client.tui
|
await ctx.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
|
|||||||
Reference in New Issue
Block a user