fix(hooks): make /stop-continuation one-time only and respect in session recovery

- Clear stop state when user sends new message (chat.message handler)
- Add isContinuationStopped check to session error recovery block
- Continuation resumes automatically after user interaction
This commit is contained in:
justsisyphus
2026-01-31 15:24:27 +09:00
parent e63c568c4f
commit 4b5e38f8f8
3 changed files with 58 additions and 1 deletions

View File

@@ -103,4 +103,42 @@ describe("stop-continuation-guard", () => {
expect(guard.isStopped(session1)).toBe(true)
expect(guard.isStopped(session2)).toBe(false)
})
test("should clear stopped state on new user message (chat.message)", async () => {
// #given - a session that was stopped
const guard = createStopContinuationGuardHook(createMockPluginInput())
const sessionID = "test-session-4"
guard.stop(sessionID)
expect(guard.isStopped(sessionID)).toBe(true)
// #when - user sends a new message
await guard["chat.message"]({ sessionID })
// #then - stop state should be cleared (one-time only)
expect(guard.isStopped(sessionID)).toBe(false)
})
test("should not affect non-stopped sessions on chat.message", async () => {
// #given - a session that was never stopped
const guard = createStopContinuationGuardHook(createMockPluginInput())
const sessionID = "test-session-5"
// #when - user sends a message (session was never stopped)
await guard["chat.message"]({ sessionID })
// #then - should not throw and session remains not stopped
expect(guard.isStopped(sessionID)).toBe(false)
})
test("should handle undefined sessionID in chat.message", async () => {
// #given - a guard with a stopped session
const guard = createStopContinuationGuardHook(createMockPluginInput())
guard.stop("some-session")
// #when - chat.message is called without sessionID
await guard["chat.message"]({ sessionID: undefined })
// #then - should not throw and stopped session remains stopped
expect(guard.isStopped("some-session")).toBe(true)
})
})

View File

@@ -5,6 +5,7 @@ const HOOK_NAME = "stop-continuation-guard"
export interface StopContinuationGuard {
event: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
"chat.message": (input: { sessionID?: string }) => Promise<void>
stop: (sessionID: string) => void
isStopped: (sessionID: string) => boolean
clear: (sessionID: string) => void
@@ -45,8 +46,20 @@ export function createStopContinuationGuardHook(
}
}
const chatMessage = async ({
sessionID,
}: {
sessionID?: string
}): Promise<void> => {
if (sessionID && stoppedSessions.has(sessionID)) {
clear(sessionID)
log(`[${HOOK_NAME}] Cleared stop state on new user message`, { sessionID })
}
}
return {
event,
"chat.message": chatMessage,
stop,
isStopped,
clear,

View File

@@ -429,6 +429,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}
}
await stopContinuationGuard?.["chat.message"]?.(input);
await keywordDetector?.["chat.message"]?.(input, output);
await claudeCodeHooks["chat.message"]?.(input, output);
await autoSlashCommand?.["chat.message"]?.(input, output);
@@ -591,7 +592,12 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const recovered =
await sessionRecovery.handleSessionRecovery(messageInfo);
if (recovered && sessionID && sessionID === getMainSessionID()) {
if (
recovered &&
sessionID &&
sessionID === getMainSessionID() &&
!stopContinuationGuard?.isStopped(sessionID)
) {
await ctx.client.session
.prompt({
path: { id: sessionID },