fix(chat): handle question and todo state
This commit is contained in:
+122
-447
@@ -8,9 +8,7 @@ import { MemoryStore } from "../memory/store.js";
|
||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import { type ResultReferenceResolver } from "../results/resolver.js";
|
||||
import { RESULT_REFERENCE_KIND } from "../results/store.js";
|
||||
import {
|
||||
type PermissionReply,
|
||||
type OpencodeRuntimeAdapter,
|
||||
} from "../runtime/opencode.js";
|
||||
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
|
||||
@@ -22,13 +20,36 @@ import {
|
||||
generateSessionTitle,
|
||||
shouldGenerateSessionTitle,
|
||||
} from "./chatSession.js";
|
||||
import { registerChatAuxiliaryRoutes } from "./chatAuxiliaryRoutes.js";
|
||||
import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js";
|
||||
import {
|
||||
collectTextContent,
|
||||
type PermissionRequestPayload,
|
||||
type QuestionRequestPayload,
|
||||
streamPromptResponse,
|
||||
supportedModels,
|
||||
type SupportedModel,
|
||||
type TodoUpdatePayload,
|
||||
} from "./chatStream.js";
|
||||
import {
|
||||
type ActiveRun,
|
||||
type RunStatus,
|
||||
type StreamSubscriber,
|
||||
cancelBackendTodos,
|
||||
completeBackendProgress,
|
||||
countFrontendUserMessages,
|
||||
createInitialStreamingMessages,
|
||||
isObjectRecord,
|
||||
pruneBranchGroupsForMessageIndex,
|
||||
toFrontendPermission,
|
||||
toPermissionStatus,
|
||||
updateLastAssistantMessage,
|
||||
updateLastAssistantPermission,
|
||||
updateLastAssistantQuestion,
|
||||
upsertBackendProgress,
|
||||
upsertBackendQuestion,
|
||||
upsertBackendTodoUpdate,
|
||||
} from "./chatUiState.js";
|
||||
|
||||
const payloadSchema = z.object({
|
||||
message: z.string().min(1).max(10000),
|
||||
@@ -38,16 +59,6 @@ const payloadSchema = z.object({
|
||||
regenerate_from_message_index: z.coerce.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
const abortPayloadSchema = z.object({
|
||||
session_id: z.string().max(128),
|
||||
});
|
||||
|
||||
const permissionReplyPayloadSchema = z.object({
|
||||
session_id: z.string().max(128),
|
||||
reply: z.enum(["once", "always", "reject"]),
|
||||
message: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
const createSessionPayloadSchema = z.object({
|
||||
session_id: z.string().max(128).optional(),
|
||||
parent_session_id: z.string().max(128).optional(),
|
||||
@@ -65,22 +76,6 @@ const sessionStateSchema = z.object({
|
||||
branch_groups: z.array(z.unknown()).default([]),
|
||||
});
|
||||
|
||||
type RunStatus = "running" | "completed" | "error" | "aborted";
|
||||
|
||||
type StreamSubscriber = {
|
||||
write: (event: string, data: Record<string, unknown>) => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
type ActiveRun = {
|
||||
clientSessionId: string;
|
||||
controller: AbortController;
|
||||
messages: unknown[];
|
||||
pendingPermissions: Map<string, PermissionRequestPayload>;
|
||||
status: RunStatus;
|
||||
subscribers: Set<StreamSubscriber>;
|
||||
};
|
||||
|
||||
const activeRuns = new Map<string, ActiveRun>();
|
||||
const lastRunStatuses = new Map<string, RunStatus>();
|
||||
|
||||
@@ -91,174 +86,6 @@ const toSessionUiStateContext = (sessionRecord: SessionRecord) => ({
|
||||
const getSessionRunStatus = (sessionId: string) =>
|
||||
activeRuns.get(sessionId)?.status ?? lastRunStatuses.get(sessionId);
|
||||
|
||||
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const createFrontendMessageId = () =>
|
||||
`msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const createInitialStreamingMessages = (existingMessages: unknown[], userContent: string) => {
|
||||
const userMessage = {
|
||||
id: createFrontendMessageId(),
|
||||
role: "user",
|
||||
content: userContent,
|
||||
};
|
||||
return [
|
||||
...existingMessages,
|
||||
{
|
||||
...userMessage,
|
||||
branchRootId: userMessage.id,
|
||||
},
|
||||
{
|
||||
id: createFrontendMessageId(),
|
||||
role: "assistant",
|
||||
content: "",
|
||||
progress: [
|
||||
{
|
||||
id: "request-received",
|
||||
phase: "start",
|
||||
status: "running",
|
||||
title: "已收到请求,正在启动 Agent 分析",
|
||||
detail: "已接收用户消息,正在建立会话并准备进入分析、规划和工具调用阶段。",
|
||||
startedAt: Date.now(),
|
||||
elapsedMs: 0,
|
||||
elapsedSnapshotAt: Date.now(),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const countFrontendUserMessages = (messages: unknown[]) =>
|
||||
messages.filter(
|
||||
(message) => isObjectRecord(message) && message.role === "user",
|
||||
).length;
|
||||
|
||||
const pruneBranchGroupsForMessageIndex = (
|
||||
branchGroups: unknown[],
|
||||
messageIndex: number | undefined,
|
||||
) => {
|
||||
if (messageIndex === undefined) {
|
||||
return branchGroups;
|
||||
}
|
||||
return branchGroups.filter(
|
||||
(group) =>
|
||||
!isObjectRecord(group) ||
|
||||
typeof group.parentCount !== "number" ||
|
||||
group.parentCount < messageIndex,
|
||||
);
|
||||
};
|
||||
|
||||
const upsertBackendProgress = (
|
||||
progress: unknown,
|
||||
payload: Record<string, unknown>,
|
||||
) => {
|
||||
const next = Array.isArray(progress) ? [...progress] : [];
|
||||
const id = typeof payload.id === "string" ? payload.id : `progress-${Date.now()}`;
|
||||
const index = next.findIndex((item) => isObjectRecord(item) && item.id === id);
|
||||
const nextItem = {
|
||||
id,
|
||||
phase: typeof payload.phase === "string" ? payload.phase : "progress",
|
||||
status:
|
||||
payload.status === "completed" || payload.status === "error"
|
||||
? payload.status
|
||||
: "running",
|
||||
title: typeof payload.title === "string" ? payload.title : "正在处理",
|
||||
detail: typeof payload.detail === "string" ? payload.detail : undefined,
|
||||
startedAt: typeof payload.started_at === "number" ? payload.started_at : undefined,
|
||||
endedAt: typeof payload.ended_at === "number" ? payload.ended_at : undefined,
|
||||
elapsedMs: typeof payload.elapsed_ms === "number" ? payload.elapsed_ms : undefined,
|
||||
elapsedSnapshotAt:
|
||||
typeof payload.elapsed_ms === "number" ? Date.now() : undefined,
|
||||
durationMs: typeof payload.duration_ms === "number" ? payload.duration_ms : undefined,
|
||||
};
|
||||
if (index >= 0) {
|
||||
next[index] = nextItem;
|
||||
} else {
|
||||
next.push(nextItem);
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const completeBackendProgress = (progress: unknown) =>
|
||||
Array.isArray(progress)
|
||||
? progress.map((item) => {
|
||||
if (!isObjectRecord(item) || item.status !== "running") {
|
||||
return item;
|
||||
}
|
||||
const endedAt = Date.now();
|
||||
const startedAt = typeof item.startedAt === "number" ? item.startedAt : undefined;
|
||||
return {
|
||||
...item,
|
||||
status: "completed",
|
||||
endedAt,
|
||||
elapsedMs: undefined,
|
||||
elapsedSnapshotAt: undefined,
|
||||
durationMs:
|
||||
typeof item.durationMs === "number"
|
||||
? item.durationMs
|
||||
: startedAt !== undefined
|
||||
? Math.max(0, endedAt - startedAt)
|
||||
: item.elapsedMs,
|
||||
};
|
||||
})
|
||||
: progress;
|
||||
|
||||
const updateLastAssistantMessage = (
|
||||
messages: unknown[],
|
||||
updater: (message: Record<string, unknown>) => Record<string, unknown>,
|
||||
) => {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (isObjectRecord(message) && message.role === "assistant") {
|
||||
const next = [...messages];
|
||||
next[index] = updater(message);
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
||||
const updateLastAssistantPermission = (
|
||||
messages: unknown[],
|
||||
requestId: string,
|
||||
updater: (permission: Record<string, unknown>) => Record<string, unknown>,
|
||||
) =>
|
||||
updateLastAssistantMessage(messages, (message) => {
|
||||
const permissions = Array.isArray(message.permissions)
|
||||
? message.permissions
|
||||
: [];
|
||||
return {
|
||||
...message,
|
||||
permissions: permissions.map((permission) =>
|
||||
isObjectRecord(permission) && permission.requestId === requestId
|
||||
? updater(permission)
|
||||
: permission,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const toFrontendPermission = (
|
||||
payload: PermissionRequestPayload,
|
||||
status: "pending" | "approved_once" | "approved_always" | "rejected" | "error" = "pending",
|
||||
) => ({
|
||||
requestId: payload.request_id,
|
||||
sessionId: payload.session_id,
|
||||
permission: payload.permission,
|
||||
patterns: payload.patterns,
|
||||
metadata: payload.metadata,
|
||||
always: payload.always,
|
||||
tool: payload.tool,
|
||||
createdAt: payload.created_at,
|
||||
status,
|
||||
});
|
||||
|
||||
const toPermissionStatus = (reply: PermissionReply) => {
|
||||
if (reply === "always") return "approved_always";
|
||||
if (reply === "once") return "approved_once";
|
||||
return "rejected";
|
||||
};
|
||||
|
||||
export const buildChatRouter = (
|
||||
sessionBridge: ChatSessionBridge,
|
||||
runtime: OpencodeRuntimeAdapter,
|
||||
@@ -580,258 +407,20 @@ export const buildChatRouter = (
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
chatRouter.get("/render-ref/:renderRef", async (req, res) => {
|
||||
const renderRef = req.params.renderRef?.trim();
|
||||
const userId = req.header("x-user-id")?.trim();
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const clientSessionId =
|
||||
typeof req.query.session_id === "string"
|
||||
? req.query.session_id.trim()
|
||||
: undefined;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
message: "x-user-id is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderRef) {
|
||||
res.status(400).json({
|
||||
message: "render_ref is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resultReferenceResolver.getFullAuthorized(
|
||||
renderRef,
|
||||
{
|
||||
actorKey: toActorKey(userId),
|
||||
clientSessionId,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ message: "render_ref not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
registerChatAuxiliaryRoutes(chatRouter, {
|
||||
activeRuns,
|
||||
lastRunStatuses,
|
||||
resultReferenceResolver,
|
||||
sessionBridge,
|
||||
sessionMetadataStore,
|
||||
sessionUiStateStore,
|
||||
});
|
||||
|
||||
chatRouter.post("/abort", async (req, res) => {
|
||||
const parsed = abortPayloadSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
message: "invalid request payload",
|
||||
detail: parsed.error.flatten(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const userId = req.header("x-user-id") ?? undefined;
|
||||
const actorKey = toActorKey(userId);
|
||||
const projectKey = toProjectKey(projectId);
|
||||
const sessionRecord = await sessionMetadataStore.get(
|
||||
{ actorKey, projectId, projectKey, userId },
|
||||
parsed.data.session_id,
|
||||
);
|
||||
const binding = sessionRecord
|
||||
? await sessionBridge.abort({
|
||||
clientSessionId: sessionRecord.sessionId,
|
||||
sessionId: sessionRecord.sessionId,
|
||||
})
|
||||
: null;
|
||||
const run = activeRuns.get(parsed.data.session_id);
|
||||
if (run && run.status === "running") {
|
||||
run.status = "aborted";
|
||||
lastRunStatuses.set(parsed.data.session_id, "aborted");
|
||||
run.controller.abort();
|
||||
run.messages = updateLastAssistantMessage(run.messages, (message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string" && message.content.trim()
|
||||
? message.content
|
||||
: "⚠️ **请求已中断**",
|
||||
isError: true,
|
||||
progress: completeBackendProgress(message.progress),
|
||||
}));
|
||||
if (sessionRecord) {
|
||||
const currentState = await sessionUiStateStore.read(
|
||||
toSessionUiStateContext(sessionRecord),
|
||||
);
|
||||
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord), {
|
||||
sessionId: sessionRecord.sessionId,
|
||||
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
|
||||
messages: run.messages,
|
||||
branchGroups: currentState?.branchGroups ?? [],
|
||||
});
|
||||
}
|
||||
for (const subscriber of run.subscribers) {
|
||||
subscriber.write("error", {
|
||||
session_id: parsed.data.session_id,
|
||||
message: "请求已中断",
|
||||
});
|
||||
subscriber.close();
|
||||
}
|
||||
run.subscribers.clear();
|
||||
}
|
||||
|
||||
if (!binding && !run) {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
clientSessionId: parsed.data.session_id,
|
||||
sessionId: binding?.sessionId ?? parsed.data.session_id,
|
||||
},
|
||||
"aborted chat session by client request",
|
||||
);
|
||||
res.status(202).json({
|
||||
session_id: parsed.data.session_id,
|
||||
aborted: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ err: error }, "chat abort failed");
|
||||
res.status(500).json({
|
||||
message: "chat abort failed",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chatRouter.post("/permission/:requestId/reply", async (req, res) => {
|
||||
const requestId = req.params.requestId?.trim();
|
||||
const parsed = permissionReplyPayloadSchema.safeParse(req.body);
|
||||
if (!requestId) {
|
||||
res.status(400).json({ message: "request_id is required" });
|
||||
return;
|
||||
}
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
message: "invalid request payload",
|
||||
detail: parsed.error.flatten(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const userId = req.header("x-user-id") ?? undefined;
|
||||
const actorKey = toActorKey(userId);
|
||||
const projectKey = toProjectKey(projectId);
|
||||
const sessionRecord = await sessionMetadataStore.get(
|
||||
{ actorKey, projectId, projectKey, userId },
|
||||
parsed.data.session_id,
|
||||
);
|
||||
if (!sessionRecord) {
|
||||
res.status(404).json({ message: "session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const run = activeRuns.get(sessionRecord.sessionId);
|
||||
if (!run || run.status !== "running") {
|
||||
res.status(409).json({ message: "session is not waiting for permissions" });
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingPermission = run.pendingPermissions.get(requestId);
|
||||
if (!pendingPermission) {
|
||||
res.status(404).json({ message: "permission request not found" });
|
||||
return;
|
||||
}
|
||||
const persistPermissionState = async () => {
|
||||
const currentState = await sessionUiStateStore.read(
|
||||
toSessionUiStateContext(sessionRecord),
|
||||
);
|
||||
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord), {
|
||||
sessionId: sessionRecord.sessionId,
|
||||
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
|
||||
messages: run.messages,
|
||||
branchGroups: currentState?.branchGroups ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await runtime.replyPermission({
|
||||
requestId,
|
||||
sessionId: sessionRecord.sessionId,
|
||||
reply: parsed.data.reply,
|
||||
message: parsed.data.message,
|
||||
});
|
||||
} catch (error) {
|
||||
run.messages = updateLastAssistantPermission(
|
||||
run.messages,
|
||||
requestId,
|
||||
(permission) => ({
|
||||
...permission,
|
||||
status: "error",
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "failed to reply permission",
|
||||
}),
|
||||
);
|
||||
await persistPermissionState().catch((persistError) => {
|
||||
logger.warn(
|
||||
{ err: persistError, sessionId: sessionRecord.sessionId },
|
||||
"failed to persist permission error state",
|
||||
);
|
||||
});
|
||||
res.status(502).json({
|
||||
message: "permission reply failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
run.pendingPermissions.delete(requestId);
|
||||
const status = toPermissionStatus(parsed.data.reply);
|
||||
run.messages = updateLastAssistantPermission(
|
||||
run.messages,
|
||||
requestId,
|
||||
(permission) => ({
|
||||
...permission,
|
||||
status,
|
||||
repliedAt: Date.now(),
|
||||
}),
|
||||
);
|
||||
await persistPermissionState().catch((persistError) => {
|
||||
logger.warn(
|
||||
{ err: persistError, sessionId: sessionRecord.sessionId },
|
||||
"failed to persist permission reply state",
|
||||
);
|
||||
});
|
||||
for (const subscriber of run.subscribers) {
|
||||
subscriber.write("permission_response", {
|
||||
session_id: sessionRecord.sessionId,
|
||||
request_id: requestId,
|
||||
reply: parsed.data.reply,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(202).json({
|
||||
session_id: sessionRecord.sessionId,
|
||||
request_id: requestId,
|
||||
reply: parsed.data.reply,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ err: error }, "permission reply route failed");
|
||||
res.status(500).json({
|
||||
message: "permission reply route failed",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
registerChatInteractionRoutes(chatRouter, {
|
||||
activeRuns,
|
||||
runtime,
|
||||
sessionMetadataStore,
|
||||
sessionUiStateStore,
|
||||
});
|
||||
|
||||
chatRouter.post("/fork", async (req, res) => {
|
||||
@@ -1045,6 +634,7 @@ export const buildChatRouter = (
|
||||
controller: abortController,
|
||||
messages: initialMessages,
|
||||
pendingPermissions: new Map(),
|
||||
pendingQuestions: new Map(),
|
||||
status: "running",
|
||||
subscribers: new Set(),
|
||||
};
|
||||
@@ -1129,6 +719,7 @@ export const buildChatRouter = (
|
||||
: `⚠️ **错误:** ${typeof data.message === "string" ? data.message : "unknown error"}`,
|
||||
isError: true,
|
||||
progress: completeBackendProgress(message.progress),
|
||||
todos: cancelBackendTodos(message.todos),
|
||||
}));
|
||||
} else if (event === "permission_request") {
|
||||
const payload = data as PermissionRequestPayload;
|
||||
@@ -1159,6 +750,60 @@ export const buildChatRouter = (
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (event === "question_request") {
|
||||
const payload = data as QuestionRequestPayload;
|
||||
let shouldTrackQuestion = true;
|
||||
if (payload.tool?.callID) {
|
||||
if (payload.request_id !== payload.tool.callID) {
|
||||
activeRun.pendingQuestions.delete(payload.tool.callID);
|
||||
} else {
|
||||
const hasActionableQuestion = [...activeRun.pendingQuestions.values()].some(
|
||||
(question) =>
|
||||
question.tool?.callID === payload.tool?.callID &&
|
||||
question.request_id !== payload.tool?.callID,
|
||||
);
|
||||
if (hasActionableQuestion) {
|
||||
activeRun.messages = updateLastAssistantMessage(
|
||||
activeRun.messages,
|
||||
(message) => ({
|
||||
...message,
|
||||
questions: upsertBackendQuestion(message.questions, payload),
|
||||
}),
|
||||
);
|
||||
shouldTrackQuestion = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldTrackQuestion) {
|
||||
activeRun.pendingQuestions.set(payload.request_id, payload);
|
||||
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
|
||||
...message,
|
||||
questions: upsertBackendQuestion(message.questions, payload),
|
||||
}));
|
||||
}
|
||||
} else if (event === "question_response") {
|
||||
const requestId =
|
||||
typeof data.request_id === "string" ? data.request_id : undefined;
|
||||
if (requestId) {
|
||||
activeRun.pendingQuestions.delete(requestId);
|
||||
activeRun.messages = updateLastAssistantQuestion(
|
||||
activeRun.messages,
|
||||
requestId,
|
||||
(question) => ({
|
||||
...question,
|
||||
status: data.rejected === true ? "rejected" : "answered",
|
||||
repliedAt: Date.now(),
|
||||
answers: Array.isArray(data.answers) ? data.answers : question.answers,
|
||||
error: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (event === "todo_update") {
|
||||
const payload = data as TodoUpdatePayload;
|
||||
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
|
||||
...message,
|
||||
todos: upsertBackendTodoUpdate(message.todos, payload),
|
||||
}));
|
||||
}
|
||||
|
||||
for (const subscriber of activeRun.subscribers) {
|
||||
@@ -1257,6 +902,21 @@ export const buildChatRouter = (
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (abortController.signal.aborted) {
|
||||
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string" && message.content.trim()
|
||||
? message.content
|
||||
: "⚠️ **请求已中断**",
|
||||
isError: true,
|
||||
progress: completeBackendProgress(message.progress),
|
||||
todos: cancelBackendTodos(message.todos),
|
||||
}));
|
||||
void queueSessionUiStatePersist().catch((error) => {
|
||||
logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist aborted chat stream state");
|
||||
});
|
||||
}
|
||||
await persistQueue.catch((error) => {
|
||||
logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
|
||||
});
|
||||
@@ -1273,7 +933,12 @@ export const buildChatRouter = (
|
||||
subscriber.close();
|
||||
}
|
||||
activeRun.subscribers.clear();
|
||||
activeRuns.delete(clientSessionId);
|
||||
if (
|
||||
activeRun.pendingPermissions.size === 0 &&
|
||||
activeRun.pendingQuestions.size === 0
|
||||
) {
|
||||
activeRuns.delete(clientSessionId);
|
||||
}
|
||||
streamClosed = true;
|
||||
req.off("close", handleClientClose);
|
||||
res.off("close", handleClientClose);
|
||||
@@ -1285,6 +950,16 @@ export const buildChatRouter = (
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ err: error }, "chat stream failed");
|
||||
if (res.headersSent) {
|
||||
if (!res.writableEnded && !res.destroyed) {
|
||||
res.write(toSse("error", {
|
||||
message: "chat stream failed",
|
||||
detail,
|
||||
}));
|
||||
res.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
res.status(500).json({
|
||||
message: "chat stream failed",
|
||||
detail,
|
||||
|
||||
Reference in New Issue
Block a user