fix(chat): restore forked context
Agent CI/CD / docker-image (push) Successful in 1m38s
Agent CI/CD / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-06-08 19:33:13 +08:00
parent 15c3263369
commit 801f611ce5
4 changed files with 44 additions and 61 deletions
+23 -41
View File
@@ -19,6 +19,7 @@ import {
extractLatestFrontendTurn, extractLatestFrontendTurn,
generateSessionTitle, generateSessionTitle,
shouldGenerateSessionTitle, shouldGenerateSessionTitle,
shouldRestoreConversationForRuntime,
} from "./chatSession.js"; } from "./chatSession.js";
import { registerChatAuxiliaryRoutes } from "./chatAuxiliaryRoutes.js"; import { registerChatAuxiliaryRoutes } from "./chatAuxiliaryRoutes.js";
import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js"; import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js";
@@ -37,10 +38,8 @@ import {
type StreamSubscriber, type StreamSubscriber,
cancelBackendTodos, cancelBackendTodos,
completeBackendProgress, completeBackendProgress,
countFrontendUserMessages,
createInitialStreamingMessages, createInitialStreamingMessages,
isObjectRecord, isObjectRecord,
pruneBranchGroupsForMessageIndex,
toFrontendPermission, toFrontendPermission,
toPermissionStatus, toPermissionStatus,
updateLastAssistantMessage, updateLastAssistantMessage,
@@ -56,7 +55,6 @@ const payloadSchema = z.object({
session_id: z.string().max(128).optional(), session_id: z.string().max(128).optional(),
model: z.enum(supportedModels).optional(), model: z.enum(supportedModels).optional(),
approval_mode: z.enum(["request", "always"]).optional().default("request"), approval_mode: z.enum(["request", "always"]).optional().default("request"),
regenerate_from_message_index: z.coerce.number().int().min(0).optional(),
}); });
const createSessionPayloadSchema = z.object({ const createSessionPayloadSchema = z.object({
@@ -86,6 +84,17 @@ const toSessionUiStateContext = (sessionRecord: SessionRecord) => ({
const getSessionRunStatus = (sessionId: string) => const getSessionRunStatus = (sessionId: string) =>
activeRuns.get(sessionId)?.status ?? lastRunStatuses.get(sessionId); activeRuns.get(sessionId)?.status ?? lastRunStatuses.get(sessionId);
const runtimeHasConversation = async (
runtime: OpencodeRuntimeAdapter,
sessionId: string,
) => {
const messages = await runtime.messages(sessionId, 1);
return messages.some(
(message) =>
message.info.role === "user" || message.info.role === "assistant",
);
};
export const buildChatRouter = ( export const buildChatRouter = (
sessionBridge: ChatSessionBridge, sessionBridge: ChatSessionBridge,
runtime: OpencodeRuntimeAdapter, runtime: OpencodeRuntimeAdapter,
@@ -555,6 +564,13 @@ export const buildChatRouter = (
userId, userId,
}); });
const activeSessionRecord = await sessionMetadataStore.touch(ensuredSessionRecord); const activeSessionRecord = await sessionMetadataStore.touch(ensuredSessionRecord);
const hasRuntimeConversation = hadExistingRuntimeSession
? await runtimeHasConversation(runtime, binding.sessionId)
: false;
const shouldRestoreConversation = shouldRestoreConversationForRuntime({
hadExistingSessionRecord: hadExistingRuntimeSession,
runtimeHasConversation: hasRuntimeConversation,
});
const historyContext = { const historyContext = {
actorKey: requestContext.actorKey, actorKey: requestContext.actorKey,
clientSessionId: requestContext.clientSessionId, clientSessionId: requestContext.clientSessionId,
@@ -565,20 +581,7 @@ export const buildChatRouter = (
toSessionUiStateContext(activeSessionRecord), toSessionUiStateContext(activeSessionRecord),
); );
const persistedMessages = initialSessionState?.messages ?? []; const persistedMessages = initialSessionState?.messages ?? [];
const isRegenerate = const baseMessages = persistedMessages;
parsed.data.regenerate_from_message_index !== undefined;
const baseMessages =
isRegenerate
? persistedMessages.slice(0, parsed.data.regenerate_from_message_index)
: persistedMessages;
const targetUserOrdinal = isRegenerate
? countFrontendUserMessages(
persistedMessages.slice(
0,
(parsed.data.regenerate_from_message_index ?? 0) + 1,
),
)
: undefined;
if (activeRuns.get(activeSessionRecord.sessionId)?.status === "running") { if (activeRuns.get(activeSessionRecord.sessionId)?.status === "running") {
res.status(409).json({ res.status(409).json({
message: "session is already streaming", message: "session is already streaming",
@@ -586,15 +589,7 @@ export const buildChatRouter = (
}); });
return; return;
} }
if (isRegenerate) { const recentTurns = await sessionTranscriptStore.getRecentTurns(historyContext, 8);
await sessionTranscriptStore.truncateThread(
historyContext,
parsed.data.regenerate_from_message_index ?? 0,
);
}
const recentTurns = isRegenerate
? []
: await sessionTranscriptStore.getRecentTurns(historyContext, 8);
logger.info( logger.info(
{ {
@@ -603,7 +598,6 @@ export const buildChatRouter = (
created: created || sessionCreated, created: created || sessionCreated,
model: parsed.data.model, model: parsed.data.model,
approvalMode: parsed.data.approval_mode, approvalMode: parsed.data.approval_mode,
regenerateFromMessageIndex: parsed.data.regenerate_from_message_index,
traceId: requestContext.traceId, traceId: requestContext.traceId,
projectId: requestContext.projectId, projectId: requestContext.projectId,
}, },
@@ -625,10 +619,7 @@ export const buildChatRouter = (
baseMessages, baseMessages,
parsed.data.message, parsed.data.message,
); );
const branchGroups = pruneBranchGroupsForMessageIndex( const branchGroups = initialSessionState?.branchGroups ?? [];
initialSessionState?.branchGroups ?? [],
parsed.data.regenerate_from_message_index,
);
const activeRun: ActiveRun = { const activeRun: ActiveRun = {
clientSessionId, clientSessionId,
controller: abortController, controller: abortController,
@@ -815,15 +806,6 @@ export const buildChatRouter = (
}; };
try { try {
if (isRegenerate) {
if (!targetUserOrdinal || targetUserOrdinal < 1) {
throw new Error("target user message not found for regeneration");
}
await runtime.revertToUserMessage(binding.sessionId, {
userOrdinal: targetUserOrdinal,
});
}
const preparedMessage = await buildPromptWithLearningContext( const preparedMessage = await buildPromptWithLearningContext(
memoryStore, memoryStore,
requestContext.actorKey, requestContext.actorKey,
@@ -832,7 +814,7 @@ export const buildChatRouter = (
recentTurns, recentTurns,
persistedMessages: baseMessages, persistedMessages: baseMessages,
message: parsed.data.message, message: parsed.data.message,
restoreConversation: !hadExistingRuntimeSession, restoreConversation: shouldRestoreConversation,
}, },
); );
const streamResult = await streamPromptResponse({ const streamResult = await streamPromptResponse({
+5
View File
@@ -212,6 +212,11 @@ export const buildPromptWithLearningContext = async (
.join("\n\n"); .join("\n\n");
}; };
export const shouldRestoreConversationForRuntime = (options: {
hadExistingSessionRecord: boolean;
runtimeHasConversation: boolean;
}) => !options.hadExistingSessionRecord || !options.runtimeHasConversation;
const buildRestoredConversationContext = (recentTurns: SessionTurnRecord[]) => { const buildRestoredConversationContext = (recentTurns: SessionTurnRecord[]) => {
const formattedTurns = recentTurns const formattedTurns = recentTurns
.slice(-RESTORE_TURN_LIMIT) .slice(-RESTORE_TURN_LIMIT)
-20
View File
@@ -63,26 +63,6 @@ export const createInitialStreamingMessages = (
]; ];
}; };
export const countFrontendUserMessages = (messages: unknown[]) =>
messages.filter(
(message) => isObjectRecord(message) && message.role === "user",
).length;
export 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,
);
};
export const upsertBackendProgress = ( export const upsertBackendProgress = (
progress: unknown, progress: unknown,
payload: Record<string, unknown>, payload: Record<string, unknown>,
+16
View File
@@ -4,6 +4,7 @@ import {
buildPromptWithLearningContext, buildPromptWithLearningContext,
extractLatestFrontendTurn, extractLatestFrontendTurn,
generateSessionTitle, generateSessionTitle,
shouldRestoreConversationForRuntime,
shouldGenerateSessionTitle, shouldGenerateSessionTitle,
} from "../../src/routes/chatSession.js"; } from "../../src/routes/chatSession.js";
import { type SessionTurnRecord } from "../../src/sessions/transcriptStore.js"; import { type SessionTurnRecord } from "../../src/sessions/transcriptStore.js";
@@ -161,6 +162,21 @@ describe("buildPromptWithLearningContext", () => {
expect(prompt).not.toContain("[Previous conversation context]"); expect(prompt).not.toContain("[Previous conversation context]");
expect(prompt).toBe("基于刚才结果继续分析"); expect(prompt).toBe("基于刚才结果继续分析");
}); });
it("restores copied fork context when metadata exists but runtime has no conversation", () => {
expect(
shouldRestoreConversationForRuntime({
hadExistingSessionRecord: true,
runtimeHasConversation: false,
}),
).toBe(true);
expect(
shouldRestoreConversationForRuntime({
hadExistingSessionRecord: true,
runtimeHasConversation: true,
}),
).toBe(false);
});
}); });
describe("extractLatestFrontendTurn", () => { describe("extractLatestFrontendTurn", () => {