Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/web/src/app/data-tasks/__tests__/conversation-restore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,7 @@ describe("shouldRestoreConversationMessages", () => {
shouldRestoreConversationMessages({
conversationMemoryEnabled: true,
isRunning: false,
replaceExistingMessages: true,
agentMessages: [{ id: "msg-user-1", role: "user", content: "old question" }],
dto,
}),
Expand Down Expand Up @@ -1230,6 +1231,7 @@ describe("shouldRestoreConversationMessages", () => {
shouldRestoreConversationMessages({
conversationMemoryEnabled: true,
isRunning: false,
replaceExistingMessages: false,
agentMessages: [
...conversationToAgentMessages(dto),
{
Expand Down Expand Up @@ -1276,6 +1278,7 @@ describe("shouldRestoreConversationMessages", () => {
shouldRestoreConversationMessages({
conversationMemoryEnabled: true,
isRunning: false,
replaceExistingMessages: false,
agentMessages: [
...conversationToAgentMessages(dto),
{
Expand All @@ -1293,6 +1296,40 @@ describe("shouldRestoreConversationMessages", () => {
}),
).toBe(false);
});

it("does not replace an existing transcript during a same-thread background refresh", () => {
const dto: SessionConversationDto = {
sessionId: "thread-1",
messages: [
{
id: "persisted-user-1",
runId: "run-1",
role: "user",
source: "client",
messageId: "persisted-user-message-1",
contentText: "你好",
position: 1,
createdAt: "2026-06-25T10:00:01Z",
},
],
runEventRefs: [],
toolCalls: [],
};

expect(
shouldRestoreConversationMessages({
conversationMemoryEnabled: true,
isRunning: false,
replaceExistingMessages: false,
agentMessages: [
{ id: "live-user-1", role: "user", content: "你好" },
{ id: "live-assistant-1", role: "assistant", content: "你好,有什么可以帮你?" },
{ id: "live-user-2", role: "user", content: "继续" },
],
dto,
}),
).toBe(false);
});
});

describe("shouldHydrateLiveRunFromConversation", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type DataTaskChatInputProps = CopilotChatInputProps & {
onEditQueuedPrompt?: (id: string, text: string) => void;
onDeleteQueuedPrompt?: (id: string) => void;
onSendQueuedPromptNow?: (id: string) => void;
/** Client-side send/upload failure shown above the composer (never silent). */
/** Current run or client-side send/upload failure shown above the composer. */
submitError?: string | null;
/** Session lock / remote-busy prompt rendered above the composer. */
sessionLockSlot?: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type DataTaskChatInputBindings = {
onUserMessageSubmitted: (text: string) => void;
liveRunStatus: LiveRunStatus;
liveRunRunId: string | null;
liveRunErrorMessage: string | null;
onCancelRun?: () => Promise<void> | void;
stopActiveRun?: () => Promise<void>;
stopActiveChatRunRef?: MutableRefObject<(() => void) | undefined>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function SessionConversationRestore({
const { setThreadRestoring, markThreadRestored } = useConversationRestoreGate();
const fetchGenerationRef = useRef(0);
const prevThreadIdRef = useRef<string | undefined>(undefined);
const messageReplacementThreadRef = useRef<string | undefined>(undefined);
const agentRef = useRef(agent);
agentRef.current = agent;
const restoreRunActive = isConversationRestoreRunActive({
Expand Down Expand Up @@ -88,6 +89,7 @@ export function SessionConversationRestore({
return;
}
agentRef.current.setMessages([]);
messageReplacementThreadRef.current = threadId;
clearRestoredInterrupts(threadId);
clearPendingCollaborationInterrupt(threadId);
clearConversationBranchSnapshot(threadId);
Expand Down Expand Up @@ -138,6 +140,9 @@ export function SessionConversationRestore({
shouldRestoreConversationMessages({
conversationMemoryEnabled,
isRunning: restoreRunActive,
replaceExistingMessages:
messageReplacementThreadRef.current === threadId ||
(currentAgent.messages?.length ?? 0) === 0,
agentMessages: currentAgent.messages,
dto: conversation,
})
Expand Down Expand Up @@ -186,6 +191,9 @@ export function SessionConversationRestore({
}
} finally {
if (!cancelled && fetchGenerationRef.current === generation) {
if (messageReplacementThreadRef.current === threadId) {
messageReplacementThreadRef.current = undefined;
}
markThreadRestored(threadId);
setThreadRestoring(threadId, false);
}
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/app/data-tasks/conversation-restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,20 @@ function hasLocalMessagesAfterRestoredPrefix(
export function shouldRestoreConversationMessages(input: {
conversationMemoryEnabled: boolean;
isRunning: boolean;
replaceExistingMessages: boolean;
agentMessages: unknown;
dto: SessionConversationDto;
}): boolean {
if (!input.conversationMemoryEnabled || input.isRunning) {
return false;
}
if (
!input.replaceExistingMessages &&
Array.isArray(input.agentMessages) &&
input.agentMessages.length > 0
) {
return false;
}
const expected = conversationToAgentMessages(input.dto);
if (expected.length === 0) {
return false;
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/data-tasks/data-tasks-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ function StableDataTaskChatInput({
onEditQueuedPrompt={handleEditQueuedPrompt}
onDeleteQueuedPrompt={handleDeleteQueuedPrompt}
onSendQueuedPromptNow={handleSendQueuedPromptNow}
submitError={submitError}
submitError={submitError ?? bindings.liveRunErrorMessage}
showDisclaimer={false}
sessionLockSlot={sessionLockSlot}
/>
Expand Down Expand Up @@ -1966,6 +1966,7 @@ function DataTaskWorkspace({
onUserMessageSubmitted: handleUserMessageSubmitted,
liveRunStatus: liveRun.runStatus,
liveRunRunId: liveRun.runId ?? null,
liveRunErrorMessage: liveRun.errorMessage ?? null,
onCancelRun: cancelCurrentRun,
stopActiveRun,
stopActiveChatRunRef,
Expand Down Expand Up @@ -2002,6 +2003,7 @@ function DataTaskWorkspace({
toggleSessionResourceItem,
sessionStartedHints,
workspaceConfig,
liveRun.errorMessage,
liveRun.runId,
liveRun.runStatus,
runCancelBusy,
Expand Down