fix(context-recovery): handle idle sessions
This commit is contained in:
@@ -101,6 +101,7 @@ describe("executeCompact lock management", () => {
|
|||||||
|
|
||||||
mockClient = {
|
mockClient = {
|
||||||
session: {
|
session: {
|
||||||
|
status: mock(() => Promise.resolve({ data: { [sessionID]: { type: "idle" } } })),
|
||||||
messages: mock(() => Promise.resolve({ data: [] })),
|
messages: mock(() => Promise.resolve({ data: [] })),
|
||||||
summarize: mock(() => Promise.resolve()),
|
summarize: mock(() => Promise.resolve()),
|
||||||
revert: mock(() => Promise.resolve()),
|
revert: mock(() => Promise.resolve()),
|
||||||
@@ -141,6 +142,25 @@ describe("executeCompact lock management", () => {
|
|||||||
expect(autoCompactState.compactionInProgress.has(sessionID)).toBe(false)
|
expect(autoCompactState.compactionInProgress.has(sessionID)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("does not start summarize recovery while the original session loop is still busy", async () => {
|
||||||
|
// given: OpenCode is still processing the context-overflow turn
|
||||||
|
mockClient.session.status = mock(() => Promise.resolve({ data: { [sessionID]: { type: "busy" } } }))
|
||||||
|
autoCompactState.errorDataBySession.set(sessionID, {
|
||||||
|
errorType: "token_limit_exceeded_unknown",
|
||||||
|
currentTokens: 0,
|
||||||
|
maxTokens: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// when: The delayed auto-compact callback fires before OpenCode reaches idle
|
||||||
|
await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig)
|
||||||
|
|
||||||
|
// then: OMO leaves recovery pending for the real session.idle event instead of racing summarize
|
||||||
|
expect(mockClient.session.summarize).not.toHaveBeenCalled()
|
||||||
|
expect(autoCompactState.pendingCompact.has(sessionID)).toBe(true)
|
||||||
|
expect(autoCompactState.errorDataBySession.has(sessionID)).toBe(true)
|
||||||
|
expect(autoCompactState.compactionInProgress.has(sessionID)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test("clears lock when summarize throws exception", async () => {
|
test("clears lock when summarize throws exception", async () => {
|
||||||
// given: Summarize will fail
|
// given: Summarize will fail
|
||||||
mockClient.session.summarize = mock(() =>
|
mockClient.session.summarize = mock(() =>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
runAggressiveTruncationStrategy,
|
runAggressiveTruncationStrategy,
|
||||||
runSummarizeRetryStrategy,
|
runSummarizeRetryStrategy,
|
||||||
} from "./recovery-strategy";
|
} from "./recovery-strategy";
|
||||||
|
import { isSessionActive } from "../shared/session-idle-settle";
|
||||||
|
import { log } from "../../shared/logger";
|
||||||
|
|
||||||
export { getLastAssistant } from "./message-builder";
|
export { getLastAssistant } from "./message-builder";
|
||||||
|
|
||||||
@@ -40,6 +42,13 @@ export async function executeCompact(
|
|||||||
autoCompactState.compactionInProgress.add(sessionID);
|
autoCompactState.compactionInProgress.add(sessionID);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (await isSessionActive(client, sessionID)) {
|
||||||
|
log("[auto-compact] delayed recovery skipped while session is still active", {
|
||||||
|
sessionID,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorData = autoCompactState.errorDataBySession.get(sessionID);
|
const errorData = autoCompactState.errorDataBySession.get(sessionID);
|
||||||
const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
|
const truncateState = getOrCreateTruncateState(autoCompactState, sessionID);
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,43 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("clears pending recovery when OpenCode core compaction succeeds first", async () => {
|
||||||
|
//#given
|
||||||
|
const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks()
|
||||||
|
const hook = createRecoveryHook()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await hook.event({
|
||||||
|
event: {
|
||||||
|
type: "session.error",
|
||||||
|
properties: { sessionID: "session-core-compacted", error: "prompt is too long" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await hook.event({
|
||||||
|
event: {
|
||||||
|
type: "session.compacted",
|
||||||
|
properties: { sessionID: "session-core-compacted" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await hook.event({
|
||||||
|
event: {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "session-core-compacted" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(getClearTimeoutCalls()).toEqual([getScheduledTimeouts()[0]])
|
||||||
|
expect(executeCompactMock).not.toHaveBeenCalled()
|
||||||
|
expect(getLastAssistantMock).toHaveBeenCalledTimes(1)
|
||||||
|
} finally {
|
||||||
|
restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("does not treat empty summary assistant messages as successful compaction", async () => {
|
test("does not treat empty summary assistant messages as successful compaction", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks()
|
const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks()
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ export function createAnthropicContextWindowLimitRecoveryHook(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === "session.compacted") {
|
||||||
|
const sessionID = resolveSessionEventID(props)
|
||||||
|
if (sessionID) {
|
||||||
|
clearSessionTimeout(pendingCompactionTimeoutBySession, sessionID)
|
||||||
|
clearSessionState(autoCompactState, sessionID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "session.error") {
|
if (event.type === "session.error") {
|
||||||
const sessionID = resolveSessionEventID(props)
|
const sessionID = resolveSessionEventID(props)
|
||||||
dependencies.log("[auto-compact] session.error received", { sessionID, error: props?.error })
|
dependencies.log("[auto-compact] session.error received", { sessionID, error: props?.error })
|
||||||
|
|||||||
Reference in New Issue
Block a user