fix(consult): paced settle is opt-in for the consultation reply; one rejection path for HTTP bodies and stream events (T2/T3 follow-up)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
Jesse_Chen
2026-09-28 09:28:37 +08:00
co-authored by Claude Fable 5.1
parent 0bc6659052
commit 891e9c62da
6 changed files with 50 additions and 40 deletions
+13 -14
View File
@@ -808,14 +808,14 @@ export function useConsultationRun(params: ConsultationRunParams) {
// run was stopped or failed, in which case what arrived shows at once.
const settleFrames = (immediate: boolean) => {
if (immediate) {
void frames.settle({ immediate: true });
void frames.settle();
return;
}
settleRequested = true;
setStreamingReply((current) => (
current && current.sessionId === sessionId ? { ...current, settling: requestId } : current
));
void frames.settle();
void frames.settle({ paced: true });
};
try {
const response = await fetch("/api/consult", {
@@ -849,16 +849,18 @@ export function useConsultationRun(params: ConsultationRunParams) {
}),
signal: controller.signal,
});
// One rejection path for the HTTP body a pre-stream check answers with
// and for the run.failed request_rejected event the stream carries once
// it is open (BUG-1074): same status semantics either way.
const rejectRun = (status: number, message: string, code?: string): never => {
if (status === 401) window.location.assign("/login");
if (status === 402) openAccountDialog("billing", { source: "insufficient-credits" });
throw new ConsultationResponseError(status, message, code);
};
if (!response.ok) {
const contentType = response.headers.get("content-type") ?? "";
const errorPayload = contentType.includes("application/json") ? await response.json() : { message: await response.text() };
if (response.status === 401) window.location.assign("/login");
if (response.status === 402) openAccountDialog("billing", { source: "insufficient-credits" });
throw new ConsultationResponseError(
response.status,
payloadMessage(errorPayload, "服务暂时不可用"),
payloadCode(errorPayload),
);
rejectRun(response.status, payloadMessage(errorPayload, "服务暂时不可用"), payloadCode(errorPayload));
}
if (!response.body) {
throw new ConsultationResponseError(502, "浏览器未收到可读取的回答流");
@@ -966,10 +968,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
// A rejection the route used to send as a JSON body with an HTTP
// status before the stream opened (BUG-1074): same status, same
// sentence, same code, so the handling below does not change.
const status = event.status ?? 503;
if (status === 401) window.location.assign("/login");
if (status === 402) openAccountDialog("billing", { source: "insufficient-credits" });
throw new ConsultationResponseError(status, payloadMessage({ message: event.message }, "服务暂时不可用"), event.reason);
rejectRun(event.status ?? 503, payloadMessage({ message: event.message }, "服务暂时不可用"), event.reason);
}
if (event.type === "run.failed") {
if (event.code === "answer_truncated") {
@@ -1086,7 +1085,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
// Whatever arrived before the failure is what gets kept, not just the
// part the pacing had released so far.
settleRequested = false;
void frames.settle({ immediate: true });
void frames.settle();
const cancelled = controller.signal.aborted;
const ownsInterface = pendingConsultation.current?.requestId === requestId;
const partialReply = latestPartialReply;