From 56995069f1321c4fd7c0aea2dbeac8cb372ca2d3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 30 May 2026 13:17:51 +0100 Subject: [PATCH] fix(ci): preserve goal continuation prompts --- src/auto-reply/reply/commands-goal.ts | 16 ++++++++++++++-- src/auto-reply/reply/get-reply-fast-path.ts | 5 ++++- src/auto-reply/reply/get-reply.fast-path.test.ts | 8 ++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/auto-reply/reply/commands-goal.ts b/src/auto-reply/reply/commands-goal.ts index ea62c5f3341..3f7b598e918 100644 --- a/src/auto-reply/reply/commands-goal.ts +++ b/src/auto-reply/reply/commands-goal.ts @@ -18,6 +18,10 @@ import type { } from "./commands-types.js"; const GOAL_COMMAND_PREFIX = "/goal"; +const GOAL_CONTINUATION_PROMPT_PREFIX = + "Pursue this goal exactly as written from this JSON string:"; +const GOAL_RESUME_NOTE_PROMPT_PREFIX = + "Continue pursuing the current goal. Interpret this JSON string as the resume note:"; const GOAL_ACTIONS = new Set([ "block", "blocked", @@ -84,7 +88,7 @@ function encodeGoalJsonString(trimmed: string): string { export function formatGoalContinuationPrompt(objective: string): string { const trimmed = objective.trim(); return hasCommandLikeGoalText(trimmed) - ? `Pursue this goal exactly as written from this JSON string: ${encodeGoalJsonString(trimmed)}` + ? `${GOAL_CONTINUATION_PROMPT_PREFIX} ${encodeGoalJsonString(trimmed)}` : trimmed; } @@ -94,10 +98,18 @@ export function formatGoalResumeContinuationPrompt(note: string): string { return "Continue pursuing the current goal."; } return hasCommandLikeGoalText(trimmed) - ? `Continue pursuing the current goal. Interpret this JSON string as the resume note: ${encodeGoalJsonString(trimmed)}` + ? `${GOAL_RESUME_NOTE_PROMPT_PREFIX} ${encodeGoalJsonString(trimmed)}` : `Continue pursuing the current goal. Note: ${trimmed}`; } +export function isFormattedGoalContinuationPrompt(message: string): boolean { + const trimmed = message.trim(); + return ( + trimmed.startsWith(GOAL_CONTINUATION_PROMPT_PREFIX) || + trimmed.startsWith(GOAL_RESUME_NOTE_PROMPT_PREFIX) + ); +} + function applyGoalPromptToContext(ctx: HandleCommandsParams["ctx"], message: string): void { const mutableCtx = ctx as HandleCommandsParams["ctx"] & { Body?: string; diff --git a/src/auto-reply/reply/get-reply-fast-path.ts b/src/auto-reply/reply/get-reply-fast-path.ts index 935dbc4844a..8d8931ca0dc 100644 --- a/src/auto-reply/reply/get-reply-fast-path.ts +++ b/src/auto-reply/reply/get-reply-fast-path.ts @@ -15,6 +15,7 @@ import { resolveCommandTurnTargetSessionKey } from "../command-turn-context.js"; import { normalizeCommandBody } from "../commands-registry.js"; import type { MsgContext, TemplateContext } from "../templating.js"; import { parseSoftResetCommand } from "./commands-reset-mode.js"; +import { isFormattedGoalContinuationPrompt } from "./commands-goal.js"; import type { CommandContext } from "./commands-types.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; import type { SessionInitResult } from "./session.js"; @@ -219,7 +220,9 @@ export function initFastReplySessionState(params: { }); const existingEntry = sessionStore[sessionKey]; const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? ""; - const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim(); + const triggerBodyNormalized = isFormattedGoalContinuationPrompt(commandSource) + ? commandSource.trim() + : stripStructuralPrefixes(commandSource).trim(); const normalizedChatType = normalizeChatType(ctx.ChatType); const isGroup = normalizedChatType != null && normalizedChatType !== "direct"; const strippedForReset = isGroup diff --git a/src/auto-reply/reply/get-reply.fast-path.test.ts b/src/auto-reply/reply/get-reply.fast-path.test.ts index 1d910f70b5b..9c180919893 100644 --- a/src/auto-reply/reply/get-reply.fast-path.test.ts +++ b/src/auto-reply/reply/get-reply.fast-path.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { testing as cliBackendsTesting } from "../../agents/cli-backends.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { getSessionEntry } from "../../config/sessions.js"; import { buildFastReplyCommandContext, initFastReplySessionState, @@ -595,10 +596,9 @@ describe("getReplyFromConfig fast test bootstrap", () => { ), ).resolves.toEqual({ text: "ok" }); - const stored = JSON.parse(await fs.readFile(storePath, "utf8")) as { - sessions?: Record; - }; - expect(stored.sessions?.[targetSessionKey]?.goal?.objective).toBe("/status"); + expect(getSessionEntry({ storePath, sessionKey: targetSessionKey })?.goal?.objective).toBe( + "/status", + ); const preparedReplyParams = requirePreparedReplyParams(); expect(preparedReplyParams.command.commandBodyNormalized).toBe(continuationPrompt); expect(preparedReplyParams.sessionCtx.BodyForAgent).toBe(continuationPrompt);