feat(consult): open the response stream before preparation, reservation and classification; rejections ride the stream (T2, BUG-1074)

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:20:23 +08:00
co-authored by Claude Fable 5.1
parent 3f8b817261
commit 0bc6659052
8 changed files with 458 additions and 5 deletions
+15 -1
View File
@@ -863,7 +863,9 @@ export function useConsultationRun(params: ConsultationRunParams) {
if (!response.body) {
throw new ConsultationResponseError(502, "浏览器未收到可读取的回答流");
}
responseKind = response.headers.get("x-jyotish-response-kind") === "smalltalk" ? "smalltalk" : undefined;
// Whether the reply was smalltalk is only known once the server has
// classified the turn, which now happens inside the stream (BUG-1074):
// the run.completed event says so; there is no response header for it.
let techniqueTruth = response.headers.get("x-jyotish-technique-truth") ?? "unknown";
let workflowReceipt: AgentExecutionReceipt["workflow"] = {
route: response.headers.get("x-jyotish-workflow-route") ?? "unknown",
@@ -891,6 +893,9 @@ export function useConsultationRun(params: ConsultationRunParams) {
label: CONSULTATION_CHART_CALCULATION_LABEL,
completedTrail: activityCompletedTrail([CONSULTATION_DONE_SKILL_LABEL]),
};
} else if (event.type === "activity" && event.phase === "received") {
// The stream's first byte, before anything has been done: no trail.
activity = { phase: event.phase, label: event.label };
} else if (event.type === "activity") {
activity = {
phase: event.phase,
@@ -957,6 +962,15 @@ export function useConsultationRun(params: ConsultationRunParams) {
techniqueTruth = event.receipt.techniqueTruth ?? "unknown";
}
}
if (event.type === "run.failed" && event.code === "request_rejected") {
// 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);
}
if (event.type === "run.failed") {
if (event.code === "answer_truncated") {
truncatedFailure = event;