From 38b1433ff58185bef9e0bc54f3f0010e0be17312 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 13 May 2026 13:09:12 +0900 Subject: [PATCH] fix(continuation): mark resumes synthetic --- .../continuation-prompt-injector.test.ts | 25 ++++++++++++++-- .../continuation-prompt-injector.ts | 4 +-- .../continuation-injection.test.ts | 25 +++++++++++++--- .../continuation-injection.ts | 4 +-- src/shared/internal-initiator-marker.test.ts | 29 +++++++++++++++++++ src/shared/internal-initiator-marker.ts | 13 +++++++++ 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.test.ts b/src/hooks/ralph-loop/continuation-prompt-injector.test.ts index bc70c859a..bc15f4734 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.test.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.test.ts @@ -61,15 +61,33 @@ describe("ralph-loop continuation prompt injector", () => { test("#given inherited message agent has ZWSP prefix #when injecting continuation prompt #then promptAsync receives normalized agent", async () => { // given - let promptBody: { agent?: string } | undefined + let promptBody: { agent?: string; noReply?: boolean } | undefined + let promptPart: + | { + text: string + synthetic?: boolean + metadata?: Record + } + | undefined const ctx = { client: { session: { messages: async () => ({ data: [{ info: { agent: "\u200bSisyphus - Ultraworker" } }], }), - promptAsync: async (input: { body: { agent?: string } }) => { + promptAsync: async (input: { + body: { + agent?: string + noReply?: boolean + parts?: Array<{ + text: string + synthetic?: boolean + metadata?: Record + }> + } + }) => { promptBody = input.body + promptPart = input.body.parts?.[0] return {} }, }, @@ -87,6 +105,9 @@ describe("ralph-loop continuation prompt injector", () => { // then expect(promptBody?.agent).toBe("sisyphus") expect(promptBody?.agent).not.toContain("\u200b") + expect(promptBody?.noReply).toBeUndefined() + expect(promptPart?.synthetic).toBe(true) + expect(promptPart?.metadata?.compaction_continue).toBe(true) }) test("#given inherited message agent has no ZWSP prefix #when injecting continuation prompt #then promptAsync receives normalized agent", async () => { diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.ts b/src/hooks/ralph-loop/continuation-prompt-injector.ts index 4fd66c7d1..dec80c5c6 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.ts @@ -4,7 +4,7 @@ import { findNearestMessageWithFields } from "../../features/hook-message-inject import { getMessageDir } from "./message-storage-directory" import { withTimeout } from "./with-timeout" import { - createInternalAgentTextPart, + createInternalAgentContinuationTextPart, isRecord, normalizeSDKResponse, resolveInheritedPromptTools, @@ -126,7 +126,7 @@ export async function injectContinuationPrompt( ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentTextPart(options.prompt)], + parts: [createInternalAgentContinuationTextPart(options.prompt)], }, query: { directory: options.directory }, }) diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts index 56dd7cb4e..8ab033ed7 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts @@ -46,7 +46,14 @@ describe("injectContinuation", () => { test("inherits tools from resolved message info when reinjecting", async () => { // given let capturedTools: Record | undefined - let capturedText: string | undefined + let capturedPart: + | { + text: string + synthetic?: boolean + metadata?: Record + } + | undefined + let capturedNoReply: boolean | undefined const ctx = { directory: "/tmp/test", client: { @@ -55,11 +62,18 @@ describe("injectContinuation", () => { promptAsync: async (input: { body: { tools?: Record - parts?: Array<{ type: string; text: string }> + noReply?: boolean + parts?: Array<{ + type: string + text: string + synthetic?: boolean + metadata?: Record + }> } }) => { capturedTools = input.body.tools - capturedText = input.body.parts?.[0]?.text + capturedNoReply = input.body.noReply + capturedPart = input.body.parts?.[0] return {} }, }, @@ -83,7 +97,10 @@ describe("injectContinuation", () => { // then expect(capturedTools).toEqual({ question: false, bash: true }) - expect(capturedText).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(capturedNoReply).toBeUndefined() + expect(capturedPart?.text).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(capturedPart?.synthetic).toBe(true) + expect(capturedPart?.metadata?.compaction_continue).toBe(true) }) test("skips injection when agent is plan (prevents Plan Mode infinite loop)", async () => { diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index d1ca73a1f..47bfaead6 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -6,7 +6,7 @@ import { resolveRegisteredAgentName, } from "../../features/claude-code-session-state" import { - createInternalAgentTextPart, + createInternalAgentContinuationTextPart, normalizeSDKResponse, resolveInheritedPromptTools, } from "../../shared" @@ -191,7 +191,7 @@ ${todoList}` ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentTextPart(prompt)], + parts: [createInternalAgentContinuationTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/shared/internal-initiator-marker.test.ts b/src/shared/internal-initiator-marker.test.ts index cc1035dd8..8ea765c25 100644 --- a/src/shared/internal-initiator-marker.test.ts +++ b/src/shared/internal-initiator-marker.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { OMO_INTERNAL_INITIATOR_MARKER, + createInternalAgentContinuationTextPart, createInternalAgentTextPart, stripInternalInitiatorMarkers, } from "./internal-initiator-marker" @@ -19,6 +20,18 @@ describe("internal-initiator-marker", () => { expect(part.text).toBe(`Hello world\n${OMO_INTERNAL_INITIATOR_MARKER}`) }) + test("#given regular internal text #when creating a text part #then leaves it visible as a normal message part", () => { + // given + const text = "Visible notification" + + // when + const part = createInternalAgentTextPart(text) + + // then + expect("synthetic" in part).toBe(false) + expect("metadata" in part).toBe(false) + }) + test("#given text already ending with the marker #when creating a text part #then does not duplicate the marker", () => { // given const text = `Already marked\n${OMO_INTERNAL_INITIATOR_MARKER}` @@ -71,6 +84,22 @@ describe("internal-initiator-marker", () => { }) }) + describe("createInternalAgentContinuationTextPart", () => { + test("#given continuation text #when creating a text part #then marks it as an agent continuation", () => { + // given + const text = "Continue the loop" + + // when + const part = createInternalAgentContinuationTextPart(text) + + // then + expect(part.type).toBe("text") + expect(part.text).toBe(`Continue the loop\n${OMO_INTERNAL_INITIATOR_MARKER}`) + expect(part.synthetic).toBe(true) + expect(part.metadata.compaction_continue).toBe(true) + }) + }) + describe("stripInternalInitiatorMarkers", () => { test("#given text with no markers #when stripping #then returns text trimmed at the end", () => { // given diff --git a/src/shared/internal-initiator-marker.ts b/src/shared/internal-initiator-marker.ts index 7e810a15e..e37a4ef88 100644 --- a/src/shared/internal-initiator-marker.ts +++ b/src/shared/internal-initiator-marker.ts @@ -16,3 +16,16 @@ export function createInternalAgentTextPart(text: string): { text: `${cleanText}\n${OMO_INTERNAL_INITIATOR_MARKER}`, } } + +export function createInternalAgentContinuationTextPart(text: string): { + type: "text" + text: string + synthetic: true + metadata: { compaction_continue: true } +} { + return { + ...createInternalAgentTextPart(text), + synthetic: true, + metadata: { compaction_continue: true }, + } +}