fix: preserve interaction replies across chat recovery

This commit is contained in:
2026-10-29 17:53:33 +08:00
parent fbe273e90a
commit 52681a8451
38 changed files with 1623 additions and 314 deletions
+19 -2
View File
@@ -5,6 +5,8 @@ import { type Router } from "express";
import { z } from "zod";
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
import { projectInteractionReplyMessages } from "../chat/interaction-message-presentation.js";
import type { InteractionRequestStore } from "../sessions/interaction-request-store.js";
import { agentModelOptions } from "../chat/models.js";
import { config } from "../config.js";
import {
@@ -76,6 +78,7 @@ const forkPayloadSchema = z.object({
});
type RegisterChatSessionRoutesOptions = {
interactionRequestStore?: InteractionRequestStore;
learningOrchestrator: LearningOrchestrator;
frontendActionCoordinator: FrontendActionCoordinator;
runState: ChatRunState;
@@ -100,6 +103,7 @@ export const registerChatSessionRoutes = (
options: RegisterChatSessionRoutesOptions,
) => {
const {
interactionRequestStore,
frontendActionCoordinator,
learningOrchestrator,
runState,
@@ -113,6 +117,17 @@ export const registerChatSessionRoutes = (
const { activeRuns, lastRunStatuses } = runState;
const getSessionRunStatus = runState.getStatus;
const withInteractionReplies = async (record: SessionRecord, messages: unknown[]) => {
if (!interactionRequestStore) return messages;
const ids = new Set(messages.flatMap((message) => {
if (!message || typeof message !== "object") return [];
const value = message as Record<string, unknown>;
return value.role === "user" && typeof value.id === "string" ? [value.id] : [];
}));
const replies = await interactionRequestStore.forMessages(toSessionUiStateContext(record), ids);
return projectInteractionReplyMessages(messages, replies);
};
const refreshCollaborationTitle = async (record: SessionRecord, credential: ProductCredential | null, knownTaskTitle?: string, signal?: AbortSignal) => {
if (readPurpose(record) !== "task_collaboration" || !record.taskId
|| !isGenericTaskCollaborationTitle(record.title)) return record;
@@ -603,8 +618,8 @@ export const registerChatSessionRoutes = (
const messages = readPurpose(record) === "task_collaboration" ? state?.messages ?? []
: activeRuns.get(record.sessionId)?.messages ?? state?.messages ?? [];
try {
res.json(sessionMessagePage(messages, { scope: historyScope([record.actorKey, record.projectKey, record.sessionId]),
...query.data }));
const page = sessionMessagePage(messages, { scope: historyScope([record.actorKey, record.projectKey, record.sessionId]), ...query.data });
res.json({ ...page, messages: await withInteractionReplies(record, page.messages) });
} catch (error) {
if (!(error instanceof HistoryCursorError)) throw error;
res.status(400).json({ message: error.message });
@@ -659,6 +674,7 @@ export const registerChatSessionRoutes = (
status: sessionRecord.status,
session_id: sessionRecord.sessionId,
...messagePayload,
messages: await withInteractionReplies(sessionRecord, messagePayload.messages),
execution_plans: state?.executionPlans ?? [],
parent_session_id: sessionRecord.parentSessionId,
is_streaming: activeRuns.get(sessionRecord.sessionId)?.status === "running",
@@ -709,6 +725,7 @@ export const registerChatSessionRoutes = (
toSse("state", {
session_id: sessionRecord.sessionId,
...messagePayload,
messages: await withInteractionReplies(sessionRecord, messagePayload.messages),
execution_plans: run ? undefined : state?.executionPlans,
is_streaming: run?.status === "running",
run_status: getSessionRunStatus(sessionRecord.sessionId),