fix(stop-continuation): persist stop state across user messages (#3276)

The /stop-continuation command was ineffective because the stop-
continuation-guard cleared its stopped state on the very next
chat.message event. Since any user message (including normal chat
after stopping) triggers chat.message, the continuation would
resume immediately.

Root cause: the chat.message handler called clear(sessionID) on
every user message, treating it as a 'user resumed work' signal.
But the user expects /stop-continuation to persist until they
explicitly start work again.

Changes:
- stop-continuation-guard chat.message: no longer clears stop state
- tool-execute-before: /start-work, /ralph-loop, /ulw-loop now
  explicitly clear the stop state (so continuation resumes when
  user intentionally restarts work)
- Updated and added tests: 12 pass (3 new), 125 related tests pass

Closes #3276
This commit is contained in:
YeonGyu-Kim
2026-04-09 21:36:10 +09:00
parent dc7a46809f
commit ab515b77d0
3 changed files with 56 additions and 6 deletions
+10 -4
View File
@@ -100,10 +100,16 @@ export function createStopContinuationGuardHook(
}: {
sessionID?: string
}): Promise<void> => {
if (sessionID && stoppedSessions.has(sessionID)) {
clear(sessionID)
log(`[${HOOK_NAME}] Cleared stop state on new user message`, { sessionID })
}
// Intentionally no-op: stop state should persist across user messages.
// Previously this cleared the stop on any new user message, but that caused
// /stop-continuation to be ineffective — the user's very next message
// (including normal chat) would re-enable continuation.
//
// Stop state is now only cleared by:
// 1. /start-work (or /ulw-loop, /ralph-loop) via explicit clear() call
// 2. session.deleted event
// 3. Future /resume-continuation command
void sessionID
}
return {
@@ -162,7 +162,7 @@ describe("stop-continuation-guard", () => {
expect(guard.isStopped(session2)).toBe(false)
})
test("should clear stopped state on new user message (chat.message)", async () => {
test("should NOT 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"
@@ -172,7 +172,38 @@ describe("stop-continuation-guard", () => {
// when - user sends a new message
await guard["chat.message"]({ sessionID })
// then - stop state should be cleared (one-time only)
// then - stop state should persist (not cleared by user messages)
// Stop is only cleared by explicit work-starting commands (/start-work, /ralph-loop, /ulw-loop)
// or session deletion. This prevents /stop-continuation from being ineffective.
expect(guard.isStopped(sessionID)).toBe(true)
})
test("should persist stop state across multiple user messages", async () => {
// given - a session that was stopped
const guard = createStopContinuationGuardHook(createMockPluginInput())
const sessionID = "test-session-persist"
guard.stop(sessionID)
// when - user sends multiple messages
await guard["chat.message"]({ sessionID })
await guard["chat.message"]({ sessionID })
await guard["chat.message"]({ sessionID })
// then - stop state remains active
expect(guard.isStopped(sessionID)).toBe(true)
})
test("should clear stop state only via explicit clear() call", () => {
// given - a session that was stopped
const guard = createStopContinuationGuardHook(createMockPluginInput())
const sessionID = "test-session-explicit-clear"
guard.stop(sessionID)
expect(guard.isStopped(sessionID)).toBe(true)
// when - clear is called (simulating /start-work or /ralph-loop)
guard.clear(sessionID)
// then - stop state is cleared
expect(guard.isStopped(sessionID)).toBe(false)
})
+13
View File
@@ -184,6 +184,19 @@ export function createToolExecuteBeforeHandler(args: {
sessionID,
})
}
// Clear stop state when user explicitly resumes work via work-starting commands.
// This ensures /stop-continuation persists until the user intentionally restarts.
const workStartingCommands = ["start-work", "ralph-loop", "ulw-loop"]
if (workStartingCommands.includes(command ?? "") && sessionID) {
if (hooks.stopContinuationGuard?.isStopped(sessionID)) {
hooks.stopContinuationGuard.clear(sessionID)
log("[stop-continuation] Stop state cleared by work-starting command", {
sessionID,
command,
})
}
}
}
}
}