refactor(chat): centralize session persistence
This commit is contained in:
+51
-77
@@ -36,6 +36,7 @@ import {
|
||||
type ActiveRun,
|
||||
type RunStatus,
|
||||
type StreamSubscriber,
|
||||
appendBackendToolArtifact,
|
||||
cancelBackendTodos,
|
||||
completeBackendProgress,
|
||||
createInitialStreamingMessages,
|
||||
@@ -67,12 +68,6 @@ const forkPayloadSchema = z.object({
|
||||
keep_message_count: z.coerce.number().int().min(0),
|
||||
});
|
||||
|
||||
const sessionStateSchema = z.object({
|
||||
title: z.string().max(120).optional(),
|
||||
is_title_manually_edited: z.boolean().optional(),
|
||||
messages: z.array(z.unknown()).default([]),
|
||||
});
|
||||
|
||||
const activeRuns = new Map<string, ActiveRun>();
|
||||
const lastRunStatuses = new Map<string, RunStatus>();
|
||||
|
||||
@@ -80,6 +75,20 @@ const toSessionUiStateContext = (sessionRecord: SessionRecord) => ({
|
||||
sessionId: sessionRecord.sessionId,
|
||||
});
|
||||
|
||||
export const buildForkedSessionUiState = (
|
||||
sourceState: { messages?: unknown[] } | null | undefined,
|
||||
input: {
|
||||
keepMessageCount: number;
|
||||
targetSessionId: string;
|
||||
},
|
||||
) => ({
|
||||
sessionId: input.targetSessionId,
|
||||
isTitleManuallyEdited: false,
|
||||
messages: Array.isArray(sourceState?.messages)
|
||||
? sourceState.messages.slice(0, input.keepMessageCount)
|
||||
: [],
|
||||
});
|
||||
|
||||
const getSessionRunStatus = (sessionId: string) =>
|
||||
activeRuns.get(sessionId)?.status ?? lastRunStatuses.get(sessionId);
|
||||
|
||||
@@ -274,71 +283,6 @@ export const buildChatRouter = (
|
||||
res.on("close", cleanup);
|
||||
});
|
||||
|
||||
chatRouter.put("/session/:sessionId", async (req, res) => {
|
||||
const sessionId = req.params.sessionId?.trim();
|
||||
const parsed = sessionStateSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
message: "invalid request payload",
|
||||
detail: parsed.error.flatten(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = req.header("x-project-id") ?? undefined;
|
||||
const userId = req.header("x-user-id") ?? undefined;
|
||||
const actorKey = toActorKey(userId);
|
||||
const projectKey = toProjectKey(projectId);
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ message: "session_id is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { record } = await sessionMetadataStore.ensure({
|
||||
actorKey,
|
||||
projectId,
|
||||
projectKey,
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
const nextRecord = await sessionMetadataStore.touch(record, {
|
||||
...(parsed.data.title ? { title: parsed.data.title } : {}),
|
||||
});
|
||||
await sessionUiStateStore.write(toSessionUiStateContext(nextRecord), {
|
||||
sessionId: nextRecord.sessionId,
|
||||
isTitleManuallyEdited: parsed.data.is_title_manually_edited,
|
||||
messages: parsed.data.messages,
|
||||
});
|
||||
const latestTurn = extractLatestFrontendTurn(parsed.data.messages);
|
||||
if (latestTurn) {
|
||||
void learningOrchestrator.onTurnCompleted({
|
||||
...latestTurn,
|
||||
requestContext: {
|
||||
actorKey,
|
||||
clientSessionId: nextRecord.sessionId,
|
||||
projectId,
|
||||
projectKey,
|
||||
traceId: req.header("x-trace-id") ?? `save-${nextRecord.sessionId}`,
|
||||
userId,
|
||||
},
|
||||
sessionId: nextRecord.sessionId,
|
||||
}).catch((error) => {
|
||||
logger.warn(
|
||||
{ err: error, sessionId: nextRecord.sessionId },
|
||||
"post-save learning failed",
|
||||
);
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
id: nextRecord.sessionId,
|
||||
title: nextRecord.title ?? "新对话",
|
||||
created_at: nextRecord.createdAt,
|
||||
updated_at: nextRecord.updatedAt,
|
||||
status: nextRecord.status,
|
||||
session_id: nextRecord.sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.patch("/session/:sessionId/title", async (req, res) => {
|
||||
const sessionId = req.params.sessionId?.trim();
|
||||
const title =
|
||||
@@ -469,7 +413,7 @@ export const buildChatRouter = (
|
||||
});
|
||||
const nextSessionId = targetSessionRecord.sessionId;
|
||||
|
||||
if (sourceSessionId && parsed.data.keep_message_count > 0) {
|
||||
if (sourceSessionId) {
|
||||
await sessionTranscriptStore.cloneThread(
|
||||
{
|
||||
actorKey,
|
||||
@@ -485,12 +429,24 @@ export const buildChatRouter = (
|
||||
},
|
||||
parsed.data.keep_message_count,
|
||||
);
|
||||
if (sourceSessionRecord?.title) {
|
||||
await sessionMetadataStore.touch(targetSessionRecord, {
|
||||
title: sourceSessionRecord.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
const sourceState = sourceSessionRecord
|
||||
? await sessionUiStateStore.read(toSessionUiStateContext(sourceSessionRecord))
|
||||
: null;
|
||||
const forkTitle = sourceSessionRecord?.title
|
||||
? `${sourceSessionRecord.title} 副本`
|
||||
: "新对话副本";
|
||||
const titledTargetSessionRecord = await sessionMetadataStore.touch(
|
||||
targetSessionRecord,
|
||||
{ title: forkTitle },
|
||||
);
|
||||
await sessionUiStateStore.write(
|
||||
toSessionUiStateContext(titledTargetSessionRecord),
|
||||
buildForkedSessionUiState(sourceState, {
|
||||
keepMessageCount: parsed.data.keep_message_count,
|
||||
targetSessionId: nextSessionId,
|
||||
}),
|
||||
);
|
||||
|
||||
logger.info(
|
||||
{
|
||||
@@ -789,6 +745,11 @@ export const buildChatRouter = (
|
||||
...message,
|
||||
todos: upsertBackendTodoUpdate(message.todos, payload),
|
||||
}));
|
||||
} else if (event === "tool_call") {
|
||||
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
|
||||
...message,
|
||||
artifacts: appendBackendToolArtifact(message.artifacts, data),
|
||||
}));
|
||||
}
|
||||
|
||||
for (const subscriber of activeRun.subscribers) {
|
||||
@@ -876,6 +837,19 @@ export const buildChatRouter = (
|
||||
logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
|
||||
});
|
||||
}
|
||||
const latestTurn = extractLatestFrontendTurn(activeRun.messages);
|
||||
if (latestTurn) {
|
||||
void learningOrchestrator.onTurnCompleted({
|
||||
...latestTurn,
|
||||
requestContext,
|
||||
sessionId: clientSessionId,
|
||||
}).catch((error) => {
|
||||
logger.warn(
|
||||
{ err: error, sessionId: clientSessionId },
|
||||
"stream-completed learning failed",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (abortController.signal.aborted) {
|
||||
|
||||
Reference in New Issue
Block a user