feat(rectification): remove the rectification regenerate button and endpoint (BUG-1056)

T2 of TASK-rectification-grounding-20260927 (product decision P1).
The rectification 「重试(重新生成)」 was a blind agent.generate rewrite that
persisted unchecked text (invented range / fit rate reproduced). Removed:
the regenerate route, regenerate-turn.ts, the regeneration agent and its
read-only tool set, the client regenerate action/state and the
regenerating/canRegenerate props. ChatMessageActions renders the regenerate
button only when onRegenerate is passed; ordinary consultation is unchanged.
The DB function regenerate_agentic_rectification_turn is kept (AGENTS §7.6,
retire in a later round). DESIGN.md / VOICE.md updated; source-contract
tests follow with 原值/新值/原因 notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-27 03:28:27 +08:00
co-authored by Claude Opus 5.5
parent 5a8084494b
commit 84eb23152a
37 changed files with 200 additions and 961 deletions
@@ -21,10 +21,11 @@ export function ChatMessageActions({
}: Readonly<{
feedback?: ChatMessageFeedback;
copied?: boolean;
canRegenerate: boolean;
/** Without `onRegenerate` the regenerate button is not rendered (rectification, BUG-1056). */
canRegenerate?: boolean;
onFeedback: (value: ChatMessageFeedback) => void;
onCopy: () => void;
onRegenerate: () => void;
onRegenerate?: () => void;
}>) {
return (
<div className="message-actions" aria-label="Agent 回答操作">
@@ -51,15 +52,17 @@ export function ChatMessageActions({
<button aria-label="复制回答" title="复制" type="button" onClick={onCopy}>
{copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
</button>
<button
aria-label="重新生成回答"
disabled={!canRegenerate}
title={canRegenerate ? "重新生成" : "只能重新生成最近一条回答"}
type="button"
onClick={onRegenerate}
>
<RotateCcw aria-hidden="true" />
</button>
{onRegenerate && (
<button
aria-label="重新生成回答"
disabled={!canRegenerate}
title={canRegenerate ? "重新生成" : "只能重新生成最近一条回答"}
type="button"
onClick={onRegenerate}
>
<RotateCcw aria-hidden="true" />
</button>
)}
</div>
);
}
@@ -58,10 +58,7 @@ import {
type RectificationStructuredChoiceOverride,
} from "@/lib/rectification-chat-choice-run";
import { runRectificationCandidateAccept } from "@/lib/rectification-chat-accept-run";
import {
copyRectificationMessage,
regenerateRectificationMessage,
} from "@/lib/rectification-chat-message-actions";
import { copyRectificationMessage } from "@/lib/rectification-chat-message-actions";
import {
refetchRectificationQuestion,
repairRectificationQuestion,
@@ -177,13 +174,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const [acceptingCandidateId, setAcceptingCandidateId] = useState<string | null>(null);
const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
const [choiceNonce, setChoiceNonce] = useState(0);
const messageActionsRef = useRef<RectificationMessageActions>({
submitChoice() {},
submitStop() {},
copyMessage() {},
regenerateMessage() {},
onFeedback() {},
});
const conversation = useRef<HTMLElement>(null);
@@ -390,23 +385,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
await copyRectificationMessage({ setCopiedMessageKey }, message);
}
async function regenerateMessage(message: RenderMessage) {
await regenerateRectificationMessage({
caseId,
sessionId,
busy,
readonly,
regeneratingMessageKey,
setError,
setMessages,
setRegeneratingMessageKey,
}, message);
}
async function submit(event: React.FormEvent) {
event.preventDefault();
if (readonly) return;
if (busy || regeneratingMessageKey) {
if (busy) {
if (queued.enqueue(draft)) setDraft("");
return;
}
@@ -416,7 +398,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
const {
latestRegeneratableKey,
latestSettledAssistant,
showLiveChoiceCard,
timelineView,
@@ -436,7 +417,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
messages,
busy,
readonly,
regeneratingMessageKey,
shouldStartOpening,
openingRequested,
acceptingCandidateId,
@@ -529,7 +509,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
submitChoice,
submitStop,
copyMessage,
regenerateMessage,
onFeedback(renderKey, requested) {
setFeedback((current) => ({
...current,
@@ -564,7 +543,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
ref={conversation}
className="conversation is-rectification"
aria-label="生时校正对话"
aria-busy={busy || regeneratingMessageKey !== null}
aria-busy={busy}
>
<div className="message-list">
{resultNotice && <p role="status" className="rectification-pending-note">{resultNotice}</p>}
@@ -585,11 +564,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{messages.map((message) => {
// D3: the same message with the persisted question placed on it.
const entryMessage = placedByKey?.get(message.renderKey) ?? message;
const regenerating = regeneratingMessageKey === message.renderKey;
const canRegenerate = message.renderKey === latestRegeneratableKey
&& !busy
&& !readonly
&& regeneratingMessageKey === null;
return (
<div
key={message.renderKey}
@@ -599,10 +573,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
message={entryMessage}
busy={busy}
readonly={readonly}
regenerating={regenerating}
canRegenerate={canRegenerate}
currentQuestionFocusId={currentQuestion?.focus_id ?? null}
interactive={!busy && !readonly && regeneratingMessageKey === null}
interactive={!busy && !readonly}
liveChoiceCard={choiceCard}
choiceNonce={choiceNonce}
savedTime={savedTime}
@@ -615,7 +587,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
<RectificationRangeDelivery
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly || regeneratingMessageKey !== null || (busy && !acceptingCandidateId)}
readonly={readonly || (busy && !acceptingCandidateId)}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
adoptLocked={liveInterviewBlockingAdopt}
/>
@@ -8,7 +8,7 @@ import {
} from "@/components/chat-message-actions";
import { ChatMessageRow } from "@/components/chat-message-row";
import { RectificationChoiceCard } from "@/components/rectification-choice-card";
import { emptyActivityTrace, type AgentActivityTraceItem } from "@/lib/agent-activity-trace";
import type { AgentActivityTraceItem } from "@/lib/agent-activity-trace";
import type { ChatMessageView } from "@/lib/chat-message-view";
import {
noteSettledRowRender,
@@ -48,7 +48,6 @@ export type RectificationMessageActions = Readonly<{
submitChoice: (key: ChoiceKey) => void;
submitStop: () => void;
copyMessage: (message: RenderMessage) => void;
regenerateMessage: (message: RenderMessage) => void;
onFeedback: (renderKey: string, requested: ChatMessageFeedback) => void;
}>;
@@ -56,8 +55,6 @@ export type RectificationMessageEntryProps = Readonly<{
message: RenderMessage;
busy: boolean;
readonly: boolean;
regenerating: boolean;
canRegenerate: boolean;
currentQuestionFocusId: string | null;
interactive: boolean;
liveChoiceCard: ChoiceCardModel | null;
@@ -102,8 +99,6 @@ export function choiceCardFromQuestion(
function RectificationMessageEntryView({
message,
busy,
regenerating,
canRegenerate,
currentQuestionFocusId,
interactive,
liveChoiceCard,
@@ -117,30 +112,19 @@ function RectificationMessageEntryView({
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text);
const displayedMessage: RenderMessage = regenerating
? {
...message,
text: "",
thinkingText: undefined,
state: "thinking" as const,
activity: undefined,
activityTrace: emptyActivityTrace(),
completedReceipt: undefined,
timeline: [],
}
: {
...message,
timeline: rectificationTimelineRows({
trace: message.activityTrace,
receipt: message.completedReceipt,
activity: message.activity,
settled: message.state === "settled",
}),
};
const displayedMessage: RenderMessage = {
...message,
timeline: rectificationTimelineRows({
trace: message.activityTrace,
receipt: message.completedReceipt,
activity: message.activity,
settled: message.state === "settled",
}),
};
const vargaSentence = !message.failed
? vargaSentenceFromMethods(message.completedReceipt?.methods)
: null;
const question = regenerating ? undefined : message.question;
const question = message.question;
const liveQuestion = Boolean(
question
&& currentQuestionFocusId === question.focus_id
@@ -193,7 +177,7 @@ function RectificationMessageEntryView({
return (
<>
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
{(!message.failed || Boolean(displayedMessage.text)) && (
<ChatMessageRow
message={displayedMessage}
showActivity={displayedMessage.state !== "settled"}
@@ -202,14 +186,13 @@ function RectificationMessageEntryView({
stoppedNotice={message.stopped ? RECTIFICATION_STOPPED_NOTICE : undefined}
/>
)}
{showActions && !regenerating && (
{/* BUG-1056: copy and feedback only; the rectification reply has no rewrite action. */}
{showActions && (
<ChatMessageActions
feedback={feedback}
copied={copied}
canRegenerate={canRegenerate}
onFeedback={(requested) => actionsRef.current.onFeedback(message.renderKey, requested)}
onCopy={() => void actionsRef.current.copyMessage(message)}
onRegenerate={() => void actionsRef.current.regenerateMessage(message)}
/>
)}
</>
@@ -219,24 +202,20 @@ function RectificationMessageEntryView({
export const RectificationMessageEntry = memo(function RectificationMessageEntry(
props: RectificationMessageEntryProps,
) {
if (props.regenerating || props.message.state !== "settled") noteStreamingRowRender();
if (props.message.state !== "settled") noteStreamingRowRender();
else noteSettledRowRender();
return <RectificationMessageEntryView {...props} />;
});
export function UnsplitRectificationMessageList({
messages,
latestRegeneratableKey,
copiedMessageKey,
feedbackByKey,
regeneratingMessageKey,
...shared
}: Omit<RectificationMessageEntryProps, "message" | "regenerating" | "canRegenerate" | "copied" | "feedback"> & {
}: Omit<RectificationMessageEntryProps, "message" | "copied" | "feedback"> & {
messages: readonly RenderMessage[];
latestRegeneratableKey?: string;
copiedMessageKey: string | null;
feedbackByKey: Readonly<Record<string, ChatMessageFeedback | undefined>>;
regeneratingMessageKey: string | null;
}) {
noteUnsplitListRender();
return (
@@ -249,8 +228,6 @@ export function UnsplitRectificationMessageList({
key={message.renderKey}
{...shared}
message={message}
regenerating={regeneratingMessageKey === message.renderKey}
canRegenerate={message.renderKey === latestRegeneratableKey && shared.interactive}
copied={copiedMessageKey === message.renderKey}
feedback={feedbackByKey[message.renderKey]}
/>