Merge pull request #2766 from code-yeongyu/fix/issue-390

fix: trigger compaction before continue after session error recovery (fixes #390)
This commit is contained in:
YeonGyu-Kim
2026-03-23 18:39:50 +09:00
committed by GitHub
2 changed files with 133 additions and 0 deletions

View File

@@ -585,3 +585,123 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
expect(promptCalls).toEqual([sessionID, sessionID])
})
})
describe("createEventHandler - session recovery compaction", () => {
it("triggers compaction before sending continue after session error recovery", async () => {
//#given
const sessionID = "ses_recovery_compaction"
setMainSession(sessionID)
const callOrder: string[] = []
const eventHandler = createEventHandler({
ctx: {
directory: "/tmp",
client: {
session: {
abort: async () => ({}),
summarize: async () => {
callOrder.push("summarize")
return {}
},
prompt: async () => {
callOrder.push("prompt")
return {}
},
},
},
} as any,
pluginConfig: {} as any,
firstMessageVariantGate: {
markSessionCreated: () => {},
clear: () => {},
},
managers: {
tmuxSessionManager: {
onSessionCreated: async () => {},
onSessionDeleted: async () => {},
},
} as any,
hooks: {
sessionRecovery: {
isRecoverableError: () => true,
handleSessionRecovery: async () => true,
},
stopContinuationGuard: { isStopped: () => false },
} as any,
})
//#when
await eventHandler({
event: {
type: "session.error",
properties: {
sessionID,
messageID: "msg_123",
error: { name: "Error", message: "tool_result block(s) that are not immediately" },
},
},
} as any)
//#then - summarize (compaction) must be called before prompt (continue)
expect(callOrder).toEqual(["summarize", "prompt"])
})
it("sends continue even if compaction fails", async () => {
//#given
const sessionID = "ses_recovery_compaction_fail"
setMainSession(sessionID)
const callOrder: string[] = []
const eventHandler = createEventHandler({
ctx: {
directory: "/tmp",
client: {
session: {
abort: async () => ({}),
summarize: async () => {
callOrder.push("summarize")
throw new Error("compaction failed")
},
prompt: async () => {
callOrder.push("prompt")
return {}
},
},
},
} as any,
pluginConfig: {} as any,
firstMessageVariantGate: {
markSessionCreated: () => {},
clear: () => {},
},
managers: {
tmuxSessionManager: {
onSessionCreated: async () => {},
onSessionDeleted: async () => {},
},
} as any,
hooks: {
sessionRecovery: {
isRecoverableError: () => true,
handleSessionRecovery: async () => true,
},
stopContinuationGuard: { isStopped: () => false },
} as any,
})
//#when
await eventHandler({
event: {
type: "session.error",
properties: {
sessionID,
messageID: "msg_456",
error: { name: "Error", message: "tool_result block(s) that are not immediately" },
},
},
} as any)
//#then - continue is still sent even when compaction fails
expect(callOrder).toEqual(["summarize", "prompt"])
})
})

View File

@@ -148,6 +148,8 @@ export function createEventHandler(args: {
body: { parts: Array<{ type: "text"; text: string }> };
query: { directory: string };
}) => Promise<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
summarize: (...args: any[]) => Promise<unknown>;
};
};
};
@@ -502,6 +504,17 @@ export function createEventHandler(args: {
sessionID === getMainSessionID() &&
!hooks.stopContinuationGuard?.isStopped(sessionID)
) {
// Trigger compaction before sending "continue" to avoid double-sending continuation
await pluginContext.client.session
.summarize({
path: { id: sessionID },
body: { auto: true },
query: { directory: pluginContext.directory },
})
.catch((err: unknown) => {
log("[event] compaction before recovery continue failed:", { sessionID, error: err });
});
await pluginContext.client.session
.prompt({
path: { id: sessionID },