From c9461a908574c16408bbcf652d80d73ec2bfe608 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 21:36:40 +0900 Subject: [PATCH] fix(stop-continuation): scope start-work clearing to fallback template Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin/chat-message.test.ts | 46 ++++++++++++++++++++++++++++----- src/plugin/chat-message.ts | 30 ++++++++++++++++----- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts index 85ab7fc89..6ecac08bb 100644 --- a/src/plugin/chat-message.test.ts +++ b/src/plugin/chat-message.test.ts @@ -17,6 +17,18 @@ import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/s type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } type ChatMessageHandlerOutput = { message: Record; parts: ChatMessagePart[] } +function createStartWorkTemplateOutput(): ChatMessageHandlerOutput { + return { + message: {}, + parts: [ + { + type: "text", + text: `context\nYou are starting a Sisyphus work session.`, + }, + ], + } +} + function createStopContinuationGuardMock(isStopped: boolean) { const clearCalls: string[] = [] const isStoppedCalls: string[] = [] @@ -248,10 +260,7 @@ describe("createChatMessageHandler - stop continuation clearing for raw slash fa }, } const handler = createChatMessageHandler(args) - const output: ChatMessageHandlerOutput = { - message: {}, - parts: [{ type: "text", text: "/start-work" }], - } + const output = createStartWorkTemplateOutput() // when await handler(createMockInput("sisyphus"), output) @@ -322,6 +331,31 @@ describe("createChatMessageHandler - stop continuation clearing for raw slash fa expect(stopContinuationGuard.clearCalls).toEqual(["test-session"]) }) + test("does not clear stop state for ordinary stopped chat messages", 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) + + // when + await handler(createMockInput("sisyphus"), { + message: {}, + parts: [{ type: "text", text: "continue helping with this bug" }], + }) + + // then + expect(startWorkCalls).toEqual(["test-session"]) + expect(stopContinuationGuard.isStoppedCalls).toHaveLength(0) + expect(stopContinuationGuard.clearCalls).toHaveLength(0) + }) + test("does not clear stop state when the session was not stopped", async () => { // given const stopContinuationGuard = createStopContinuationGuardMock(false) @@ -346,7 +380,7 @@ describe("createChatMessageHandler - stop continuation clearing for raw slash fa // when await handler(createMockInput("sisyphus"), { message: {}, - parts: [{ type: "text", text: "/start-work" }], + parts: createStartWorkTemplateOutput().parts, }) await handler(createMockInput("sisyphus"), { message: {}, @@ -371,8 +405,6 @@ describe("createChatMessageHandler - stop continuation clearing for raw slash fa "test-session", "test-session", "test-session", - "test-session", - "test-session", ]) expect(stopContinuationGuard.clearCalls).toHaveLength(0) }) diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index 68bdaccda..943165790 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -26,6 +26,7 @@ export type ChatMessageInput = { type StartWorkHookOutput = { parts: Array<{ type: string; text?: string }> } type SessionModelOverride = { providerID: string; modelID: string } +const START_WORK_TEMPLATE_MARKER = "You are starting a Sisyphus work session." type RawLoopCommand = | { command: "ralph-loop" | "ulw-loop"; args: string } @@ -125,6 +126,23 @@ function parseRawLoopSlashCommand(promptText: string): RawLoopCommand | null { return null } +function extractPromptText(parts: ChatMessagePart[]): string { + return ( + parts + ?.filter((part) => part.type === "text" && part.text) + .map((part) => part.text) + .join("\n") + .trim() || "" + ) +} + +function isStartWorkFallbackTemplate(promptText: string): boolean { + return ( + promptText.includes("") && + promptText.includes(START_WORK_TEMPLATE_MARKER) + ) +} + function clearStoppedContinuationBeforeWorkStart( hooks: CreatedHooks, sessionID: string, @@ -221,7 +239,10 @@ export function createChatMessageHandler(args: { await hooks.noSisyphusGpt?.["chat.message"]?.(input, output) await hooks.noHephaestusNonGpt?.["chat.message"]?.(input, output) if (hooks.startWork && isStartWorkHookOutput(output)) { - clearStoppedContinuationBeforeWorkStart(hooks, input.sessionID, "start-work") + const promptText = extractPromptText(output.parts) + if (isStartWorkFallbackTemplate(promptText)) { + clearStoppedContinuationBeforeWorkStart(hooks, input.sessionID, "start-work") + } await hooks.startWork["chat.message"]?.(input, output) } @@ -241,12 +262,7 @@ export function createChatMessageHandler(args: { if (hooks.ralphLoop && output.message[NATIVE_LOOP_TRIGGERED_FLAG] !== true) { const parts = output.parts - const promptText = - parts - ?.filter((p) => p.type === "text" && p.text) - .map((p) => p.text) - .join("\n") - .trim() || "" + const promptText = extractPromptText(parts) const isRalphLoopTemplate = promptText.includes("You are starting a Ralph Loop") &&