From c551f5ae81886ce71e9dfa1a99cc4861b6789b0b Mon Sep 17 00:00:00 2001 From: moyu Date: Mon, 8 Jun 2026 18:04:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20goal=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A4=9A=E9=A1=B9=20AI=20=E5=AE=A1=E6=9F=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prompt 中 update 行为描述与运行时不一致(no-op → error) - src/commands/goal/ 使用相对路径导入,改为 src/* 别名 - /goal 命令标记 bridgeSafe 但含交互式对话框,改为 false - useGoalContinuation 中 origin 使用 as unknown as string 强转,改为直接传字符串 - ResumeConversation 路径缺少 goal hydration,补齐恢复逻辑 - onCancel 在非查询状态下误暂停 goal,加 queryGuard 守卫 - resumeGoal 允许从终态恢复,收紧为仅允许 paused 状态 - buildGoalContextBlock 生成畸形 XML 属性,改为合法 budget 属性 --- .../goal/GoalReplaceConfirmDialog.tsx | 8 ++++---- src/commands/goal/goal.tsx | 8 ++++---- src/commands/goal/index.ts | 4 ++-- src/hooks/useGoalContinuation.ts | 20 +++++++++---------- src/screens/REPL.tsx | 5 ++++- src/screens/ResumeConversation.tsx | 9 +++++++++ src/services/goal/goalState.ts | 7 +------ src/services/goal/prompts.ts | 2 +- 8 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/commands/goal/GoalReplaceConfirmDialog.tsx b/src/commands/goal/GoalReplaceConfirmDialog.tsx index ca1376263..9baed2faa 100644 --- a/src/commands/goal/GoalReplaceConfirmDialog.tsx +++ b/src/commands/goal/GoalReplaceConfirmDialog.tsx @@ -6,10 +6,10 @@ import * as React from 'react'; import { Box, Text } from '@anthropic/ink'; -import type { GoalState } from '../../types/logs.js'; -import { Select } from '../../components/CustomSelect/index.js'; -import { PermissionDialog } from '../../components/permissions/PermissionDialog.js'; -import { formatGoalElapsed, formatGoalStatusLabel } from '../../services/goal/goalState.js'; +import type { GoalState } from 'src/types/logs.js'; +import { Select } from 'src/components/CustomSelect/index.js'; +import { PermissionDialog } from 'src/components/permissions/PermissionDialog.js'; +import { formatGoalElapsed, formatGoalStatusLabel } from 'src/services/goal/goalState.js'; type Props = { currentGoal: GoalState; diff --git a/src/commands/goal/goal.tsx b/src/commands/goal/goal.tsx index 431ffa827..72287f909 100644 --- a/src/commands/goal/goal.tsx +++ b/src/commands/goal/goal.tsx @@ -15,8 +15,8 @@ */ import * as React from 'react'; -import type { LocalJSXCommandContext } from '../../commands.js'; -import type { LocalJSXCommandOnDone } from '../../types/command.js'; +import type { LocalJSXCommandContext } from 'src/commands.js'; +import type { LocalJSXCommandOnDone } from 'src/types/command.js'; import { clearGoal, completeGoal, @@ -26,8 +26,8 @@ import { pauseGoal, resumeGoal, setGoal, -} from '../../services/goal/goalState.js'; -import { persistCurrentGoal, persistGoalClear } from '../../services/goal/goalStorage.js'; +} from 'src/services/goal/goalState.js'; +import { persistCurrentGoal, persistGoalClear } from 'src/services/goal/goalStorage.js'; import { GoalReplaceConfirmDialog } from './GoalReplaceConfirmDialog.js'; const MAX_OBJECTIVE_CHARS = 4000; diff --git a/src/commands/goal/index.ts b/src/commands/goal/index.ts index b36e5741c..f9da40aaa 100644 --- a/src/commands/goal/index.ts +++ b/src/commands/goal/index.ts @@ -1,4 +1,4 @@ -import type { Command } from '../../commands.js' +import type { Command } from 'src/commands.js' const goal = { type: 'local-jsx', @@ -6,7 +6,7 @@ const goal = { description: 'Set or view a persistent goal that drives auto-continuation across turns', argumentHint: '[ | status | clear | pause | resume | complete]', - bridgeSafe: true, + bridgeSafe: false, load: () => import('./goal.js'), } satisfies Command diff --git a/src/hooks/useGoalContinuation.ts b/src/hooks/useGoalContinuation.ts index 5f1b12a59..5c02f6cbd 100644 --- a/src/hooks/useGoalContinuation.ts +++ b/src/hooks/useGoalContinuation.ts @@ -21,22 +21,22 @@ */ import { useEffect, useRef } from 'react' -import { logForDebugging } from '../utils/debug.js' +import { logForDebugging } from 'src/utils/debug.js' import { getGoal, incrementGoalTurns, MAX_GOAL_TURNS, -} from '../services/goal/goalState.js' +} from 'src/services/goal/goalState.js' +import { persistCurrentGoal } from 'src/services/goal/goalStorage.js' +import { + buildBudgetLimitPrompt, + buildContinuationPrompt, +} from 'src/services/goal/prompts.js' +import { enqueue } from 'src/utils/messageQueueManager.js' function hookLog(msg: string): void { logForDebugging(`[goal] hook: ${msg}`) } -import { persistCurrentGoal } from '../services/goal/goalStorage.js' -import { - buildBudgetLimitPrompt, - buildContinuationPrompt, -} from '../services/goal/prompts.js' -import { enqueue } from '../utils/messageQueueManager.js' export type UseGoalContinuationOpts = { isLoading: boolean @@ -94,7 +94,7 @@ export function useGoalContinuation(opts: UseGoalContinuationOpts): void { mode: 'prompt', priority: 'later', isMeta: true, - origin: { kind: 'goal-budget-limit' } as unknown as string, + origin: 'goal-budget-limit', skipSlashCommands: true, }) return @@ -127,7 +127,7 @@ export function useGoalContinuation(opts: UseGoalContinuationOpts): void { mode: 'prompt', priority: 'later', isMeta: true, - origin: { kind: 'goal-continuation' } as unknown as string, + origin: 'goal-continuation', skipSlashCommands: true, }) }, [ diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 82ee02646..7592e3d3f 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -2544,7 +2544,10 @@ export function REPL({ // Ctrl+C during an active goal turn pauses the goal so the // continuation loop stops. The user can /goal resume to continue later. - if (feature('GOAL')) { + // Guard: only pause when a query is actually in flight. onCancel() is + // also called from the restore/edit flow (idle), and pausing then would + // incorrectly stop the next continuation. + if (feature('GOAL') && queryGuard.getSnapshot()) { const { getGoal, pauseGoal } = require('../services/goal/goalState.js') as typeof import('../services/goal/goalState.js'); const { persistCurrentGoal } = diff --git a/src/screens/ResumeConversation.tsx b/src/screens/ResumeConversation.tsx index fe07e46a1..cc2075278 100644 --- a/src/screens/ResumeConversation.tsx +++ b/src/screens/ResumeConversation.tsx @@ -1,4 +1,5 @@ import { feature } from 'bun:bundle'; +import type { UUID } from 'crypto'; import { dirname } from 'path'; import React from 'react'; import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; @@ -296,6 +297,14 @@ export function ResumeConversation({ } } + if (feature('GOAL') && log.goal && result.sessionId) { + const { hydrateGoalFromTranscript } = + require('src/services/goal/goalStorage.js') as typeof import('src/services/goal/goalStorage.js'); + const goalsMap = new Map(); + goalsMap.set(result.sessionId, log.goal); + hydrateGoalFromTranscript(goalsMap, result.sessionId); + } + if (feature('CONTEXT_COLLAPSE')) { /* eslint-disable @typescript-eslint/no-require-imports */ ( diff --git a/src/services/goal/goalState.ts b/src/services/goal/goalState.ts index 16a855095..fe17b7ffa 100644 --- a/src/services/goal/goalState.ts +++ b/src/services/goal/goalState.ts @@ -91,12 +91,7 @@ export function resumeGoal(sessionId?: string): GoalState | null { const id = resolveSessionId(sessionId) const goal = goals.get(id) if (!goal) return null - if ( - goal.status !== 'paused' && - goal.status !== 'blocked' && - goal.status !== 'budget_limited' && - goal.status !== 'usage_limited' - ) { + if (goal.status !== 'paused') { return null } const now = Date.now() diff --git a/src/services/goal/prompts.ts b/src/services/goal/prompts.ts index 209028e24..47f31684e 100644 --- a/src/services/goal/prompts.ts +++ b/src/services/goal/prompts.ts @@ -140,7 +140,7 @@ export function buildGoalContextBlock(goal: GoalState): string { const elapsedMs = getActiveElapsedMs(goal) const budget = goal.tokenBudget !== null - ? ` | budget: ${goal.tokensUsed}/${goal.tokenBudget}` + ? ` budget="${goal.tokenBudget}"` : '' return [