fix(stop-continuation): clear chat.message fallback stop state before work resumes
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -17,6 +17,27 @@ import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/s
|
|||||||
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
|
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
|
||||||
type ChatMessageHandlerOutput = { message: Record<string, unknown>; parts: ChatMessagePart[] }
|
type ChatMessageHandlerOutput = { message: Record<string, unknown>; parts: ChatMessagePart[] }
|
||||||
|
|
||||||
|
function createStopContinuationGuardMock(isStopped: boolean) {
|
||||||
|
const clearCalls: string[] = []
|
||||||
|
const isStoppedCalls: string[] = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
guard: {
|
||||||
|
"chat.message": async () => {},
|
||||||
|
stop: () => {},
|
||||||
|
isStopped: (sessionID: string) => {
|
||||||
|
isStoppedCalls.push(sessionID)
|
||||||
|
return isStopped
|
||||||
|
},
|
||||||
|
clear: (sessionID: string) => {
|
||||||
|
clearCalls.push(sessionID)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clearCalls,
|
||||||
|
isStoppedCalls,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createMockHandlerArgs(overrides?: {
|
function createMockHandlerArgs(overrides?: {
|
||||||
pluginConfig?: Record<string, unknown>
|
pluginConfig?: Record<string, unknown>
|
||||||
shouldOverride?: boolean
|
shouldOverride?: boolean
|
||||||
@@ -214,6 +235,149 @@ describe("createChatMessageHandler - /start-work integration", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("createChatMessageHandler - stop continuation clearing for raw slash fallback", () => {
|
||||||
|
test("clears stop state before raw /start-work resumes work through chat.message", async () => {
|
||||||
|
// given
|
||||||
|
const stopContinuationGuard = createStopContinuationGuardMock(true)
|
||||||
|
const startWorkCalls: string[] = []
|
||||||
|
const args = createMockHandlerArgs()
|
||||||
|
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
|
||||||
|
args.hooks.startWork = {
|
||||||
|
"chat.message": async (input: { sessionID: string }) => {
|
||||||
|
startWorkCalls.push(input.sessionID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const handler = createChatMessageHandler(args)
|
||||||
|
const output: ChatMessageHandlerOutput = {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/start-work" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await handler(createMockInput("sisyphus"), output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(startWorkCalls).toEqual(["test-session"])
|
||||||
|
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
|
||||||
|
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clears stop state before raw /ulw-loop resumes work through chat.message", async () => {
|
||||||
|
// given
|
||||||
|
const stopContinuationGuard = createStopContinuationGuardMock(true)
|
||||||
|
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
|
||||||
|
const args = createMockHandlerArgs()
|
||||||
|
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
|
||||||
|
args.hooks.ralphLoop = {
|
||||||
|
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
|
||||||
|
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
cancelLoop: () => true,
|
||||||
|
}
|
||||||
|
const handler = createChatMessageHandler(args)
|
||||||
|
const output: ChatMessageHandlerOutput = {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/ulw-loop ship it" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await handler(createMockInput("sisyphus"), output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(startLoopCalls).toEqual([
|
||||||
|
{ sessionID: "test-session", prompt: "ship it", ultrawork: true },
|
||||||
|
])
|
||||||
|
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
|
||||||
|
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clears stop state before raw /ralph-loop resumes work through chat.message", async () => {
|
||||||
|
// given
|
||||||
|
const stopContinuationGuard = createStopContinuationGuardMock(true)
|
||||||
|
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
|
||||||
|
const args = createMockHandlerArgs()
|
||||||
|
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
|
||||||
|
args.hooks.ralphLoop = {
|
||||||
|
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
|
||||||
|
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
cancelLoop: () => true,
|
||||||
|
}
|
||||||
|
const handler = createChatMessageHandler(args)
|
||||||
|
const output: ChatMessageHandlerOutput = {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/ralph-loop keep going" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await handler(createMockInput("sisyphus"), output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(startLoopCalls).toEqual([
|
||||||
|
{ sessionID: "test-session", prompt: "keep going", ultrawork: false },
|
||||||
|
])
|
||||||
|
expect(stopContinuationGuard.isStoppedCalls).toEqual(["test-session"])
|
||||||
|
expect(stopContinuationGuard.clearCalls).toEqual(["test-session"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not clear stop state when the session was not stopped", async () => {
|
||||||
|
// given
|
||||||
|
const stopContinuationGuard = createStopContinuationGuardMock(false)
|
||||||
|
const startWorkCalls: string[] = []
|
||||||
|
const startLoopCalls: Array<{ sessionID: string; prompt: string; ultrawork: boolean }> = []
|
||||||
|
const args = createMockHandlerArgs()
|
||||||
|
args.hooks.stopContinuationGuard = stopContinuationGuard.guard
|
||||||
|
args.hooks.startWork = {
|
||||||
|
"chat.message": async (input: { sessionID: string }) => {
|
||||||
|
startWorkCalls.push(input.sessionID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
args.hooks.ralphLoop = {
|
||||||
|
startLoop: (sessionID: string, prompt: string, options?: { ultrawork?: boolean }) => {
|
||||||
|
startLoopCalls.push({ sessionID, prompt, ultrawork: options?.ultrawork === true })
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
cancelLoop: () => true,
|
||||||
|
}
|
||||||
|
const handler = createChatMessageHandler(args)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await handler(createMockInput("sisyphus"), {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/start-work" }],
|
||||||
|
})
|
||||||
|
await handler(createMockInput("sisyphus"), {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/ulw-loop continue" }],
|
||||||
|
})
|
||||||
|
await handler(createMockInput("sisyphus"), {
|
||||||
|
message: {},
|
||||||
|
parts: [{ type: "text", text: "/ralph-loop continue" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(startWorkCalls).toEqual([
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
])
|
||||||
|
expect(startLoopCalls).toEqual([
|
||||||
|
{ sessionID: "test-session", prompt: "continue", ultrawork: true },
|
||||||
|
{ sessionID: "test-session", prompt: "continue", ultrawork: false },
|
||||||
|
])
|
||||||
|
expect(stopContinuationGuard.isStoppedCalls).toEqual([
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
"test-session",
|
||||||
|
])
|
||||||
|
expect(stopContinuationGuard.clearCalls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("createChatMessageHandler - /ulw-loop raw slash fallback", () => {
|
describe("createChatMessageHandler - /ulw-loop raw slash fallback", () => {
|
||||||
test("starts ultrawork loop when /ulw-loop arrives through chat.message without native command expansion", async () => {
|
test("starts ultrawork loop when /ulw-loop arrives through chat.message without native command expansion", async () => {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { OhMyOpenCodeConfig } from "../config"
|
import type { OhMyOpenCodeConfig } from "../config"
|
||||||
import type { PluginContext } from "./types"
|
import type { PluginContext } from "./types"
|
||||||
|
|
||||||
import { isModelCacheAvailable } from "../shared"
|
import { isModelCacheAvailable, log } from "../shared"
|
||||||
import { getAgentConfigKey } from "../shared/agent-display-names"
|
import { getAgentConfigKey } from "../shared/agent-display-names"
|
||||||
import { getSessionModel, setSessionModel } from "../shared/session-model-state"
|
import { getSessionModel, setSessionModel } from "../shared/session-model-state"
|
||||||
import { getMainSessionID, setSessionAgent, subagentSessions } from "../features/claude-code-session-state"
|
import { getMainSessionID, setSessionAgent, subagentSessions } from "../features/claude-code-session-state"
|
||||||
@@ -125,6 +125,20 @@ function parseRawLoopSlashCommand(promptText: string): RawLoopCommand | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearStoppedContinuationBeforeWorkStart(
|
||||||
|
hooks: CreatedHooks,
|
||||||
|
sessionID: string,
|
||||||
|
command: "start-work" | "ralph-loop" | "ulw-loop"
|
||||||
|
): void {
|
||||||
|
if (hooks.stopContinuationGuard?.isStopped(sessionID)) {
|
||||||
|
hooks.stopContinuationGuard.clear(sessionID)
|
||||||
|
log("[stop-continuation] Stop state cleared by chat.message work-starting command", {
|
||||||
|
sessionID,
|
||||||
|
command,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createChatMessageHandler(args: {
|
export function createChatMessageHandler(args: {
|
||||||
ctx: PluginContext
|
ctx: PluginContext
|
||||||
pluginConfig: OhMyOpenCodeConfig
|
pluginConfig: OhMyOpenCodeConfig
|
||||||
@@ -207,6 +221,7 @@ export function createChatMessageHandler(args: {
|
|||||||
await hooks.noSisyphusGpt?.["chat.message"]?.(input, output)
|
await hooks.noSisyphusGpt?.["chat.message"]?.(input, output)
|
||||||
await hooks.noHephaestusNonGpt?.["chat.message"]?.(input, output)
|
await hooks.noHephaestusNonGpt?.["chat.message"]?.(input, output)
|
||||||
if (hooks.startWork && isStartWorkHookOutput(output)) {
|
if (hooks.startWork && isStartWorkHookOutput(output)) {
|
||||||
|
clearStoppedContinuationBeforeWorkStart(hooks, input.sessionID, "start-work")
|
||||||
await hooks.startWork["chat.message"]?.(input, output)
|
await hooks.startWork["chat.message"]?.(input, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +267,9 @@ export function createChatMessageHandler(args: {
|
|||||||
const rawTask = taskMatch?.[1]?.trim() || rawLoopCommand?.args || ""
|
const rawTask = taskMatch?.[1]?.trim() || rawLoopCommand?.args || ""
|
||||||
const parsedArguments = parseRalphLoopArguments(rawTask)
|
const parsedArguments = parseRalphLoopArguments(rawTask)
|
||||||
const ultrawork = isUlwLoopTemplate || rawLoopCommand?.command === "ulw-loop"
|
const ultrawork = isUlwLoopTemplate || rawLoopCommand?.command === "ulw-loop"
|
||||||
|
const command = ultrawork ? "ulw-loop" : "ralph-loop"
|
||||||
|
|
||||||
|
clearStoppedContinuationBeforeWorkStart(hooks, input.sessionID, command)
|
||||||
hooks.ralphLoop.startLoop(input.sessionID, parsedArguments.prompt, {
|
hooks.ralphLoop.startLoop(input.sessionID, parsedArguments.prompt, {
|
||||||
ultrawork,
|
ultrawork,
|
||||||
maxIterations: parsedArguments.maxIterations,
|
maxIterations: parsedArguments.maxIterations,
|
||||||
|
|||||||
Reference in New Issue
Block a user