fix(rectification): enforce trusted result identity and preserve receipt provenance
Independent Staging Quality Gate / validate (push) Successful in 10m4s
Independent Staging Quality Gate / publish (push) Successful in 10m26s

Unify minute and block cache identity, keep unverifiable historical results read-only across server tools and write entrypoints, and aggregate completed receipt sources chronologically through a compatible function migration.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-20 18:03:38 +08:00
co-authored by Claude Code
parent 6dd62207eb
commit 8d0359fc62
37 changed files with 5983 additions and 95 deletions
@@ -64,6 +64,7 @@ import {
isAbortError,
rectificationConversationState,
rectificationInitialLiveLabel,
recompareHistoricalResult,
rectificationQuestionGapState,
rectificationReadonlyRangeCopy,
contrastProbesFromReceipt,
@@ -389,7 +390,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const {
caseId,
sessionId,
readonly,
readonly: historyReadonly,
shouldStartOpening,
initialTurns,
initialSnapshot,
@@ -410,6 +411,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
headerSlot,
} = props;
const [resultNotice, setResultNotice] = useState(initialSnapshot?.case?.result_notice ?? null);
const [canRecompare, setCanRecompare] = useState(initialSnapshot?.case?.result_identity?.can_recompare === true);
const readonly = historyReadonly || Boolean(resultNotice);
const conversationReadonly = historyReadonly || (Boolean(resultNotice) && !canRecompare);
const [messages, setMessages] = useState<RenderMessage[]>(() => messagesFromTurns(initialTurns));
const [draft, setDraft] = useState("");
const queued = useQueuedMessage();
@@ -583,6 +588,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
} | null) => {
if (!payload) return;
const snapshot = payload as RectificationCaseSnapshotPayload;
setResultNotice(snapshot.case?.result_notice ?? null);
setCanRecompare(snapshot.case?.result_identity?.can_recompare === true);
const nextCandidate = parseRectificationCandidateResult(payload.latest_result);
const nextQuestion = currentQuestionFromSnapshot(payload.current_question);
const nextChoice = parseRectificationChoiceCard(payload.choice_card);
@@ -693,7 +700,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
continuation?: Readonly<{ reuseAssistantRenderKey: string; label: string }>,
) => {
const trimmed = action === "message" ? messageText.trim() : "";
if ((action === "message" && !trimmed) || readonly) return;
if ((action === "message" && !trimmed) || conversationReadonly) return;
if (!continuation && busy) return;
setError("");
setPending(true);
@@ -1018,7 +1025,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
lastRunOutcome.current = readonly ? "readonly" : runOutcome;
setPending(false);
}
}, [beginLiveRun, busy, caseId, loadCaseSnapshot, onCompleted, onMessagesChange, onOpenBilling, onProfileIncomplete, readonly, rememberLiveActivity, selectedModelId, sessionId, setPending]);
}, [beginLiveRun, busy, caseId, loadCaseSnapshot, onCompleted, onMessagesChange, onOpenBilling, onProfileIncomplete, readonly, conversationReadonly, rememberLiveActivity, selectedModelId, sessionId, setPending]);
useEffect(() => {
if (busy) return;
@@ -1688,6 +1695,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
aria-busy={busy || regeneratingMessageKey !== null}
>
<div className="message-list">
{resultNotice && <p role="status" className="rectification-pending-note">{resultNotice}</p>}
{resultNotice && canRecompare && !historyReadonly && (
<Button type="button" variant="outline" disabled={busy} onClick={() => void recompareHistoricalResult({ canRecompare, historyReadonly, busy, send })}>重新比较</Button>
)}
{pendingConsultationQuestion?.trim() && (
<p className="rectification-pending-note">
先陪你核对出生时间范围;结束后新建对话,按采用的时间再问:“{pendingConsultationQuestion.trim()}”
@@ -1822,7 +1833,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
</p>
)}
{error && <p className="error-message" role="alert">{error}</p>}
{readonly && (
{historyReadonly && (
<div className="rectification-terminal-actions">
<p className="rectification-terminal-note">该校正已结束,只能查看历史。</p>
<Button type="button" onClick={onRestart}>再次校正</Button>
@@ -1839,8 +1850,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
inputRef={composer}
value={draft}
remainingId={composerRemainingId}
inputLabel={readonly ? "该校正已结束,只能查看历史" : "继续描述你的经历或回答"}
placeholder={readonly
inputLabel={resultNotice ? "当前结果仅供查看" : readonly ? "该校正已结束,只能查看历史" : "继续描述你的经历或回答"}
placeholder={resultNotice
? "当前结果仅供查看,暂不能选择或采用。"
: readonly
? "该校正已结束,只能查看历史;需要再次校正请新建。"
: showLiveChoiceCard
? "点上面的选项即可;想补一句细节再写"