修正Direct聊天中断上下文恢复\n删除打开项目自动重发旧消息\n持久化partial并在新消息前回放完整历史
This commit is contained in:
@@ -293,56 +293,6 @@ function directCodexConversationMessageId(
|
||||
return `${DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX}${turnId}:${role}`;
|
||||
}
|
||||
|
||||
function directCodexConversationTurnId(
|
||||
message: ChatMessage,
|
||||
role: ChatMessage['role'],
|
||||
) {
|
||||
if (message.role !== role) {
|
||||
return null;
|
||||
}
|
||||
const messageId = message.messageId?.trim() ?? '';
|
||||
const roleSuffix = `:${role}`;
|
||||
if (
|
||||
!messageId.startsWith(DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX) ||
|
||||
!messageId.endsWith(roleSuffix)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const turnId = messageId.slice(
|
||||
DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX.length,
|
||||
-roleSuffix.length,
|
||||
);
|
||||
return /^[a-z0-9][a-z0-9-]{5,159}$/iu.test(turnId) ? turnId : null;
|
||||
}
|
||||
|
||||
export function unansweredDirectCodexConversationTurn(messages: ChatMessage[]) {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
const turnId = directCodexConversationTurnId(message, 'user');
|
||||
if (!turnId) {
|
||||
continue;
|
||||
}
|
||||
const assistantMessageId = directCodexConversationMessageId(
|
||||
turnId,
|
||||
'assistant',
|
||||
);
|
||||
if (
|
||||
messages.some(
|
||||
(candidate) =>
|
||||
candidate.role === 'assistant' &&
|
||||
candidate.messageId?.trim() === assistantMessageId,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return { prompt: message.text, turnId };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isDirectCodexTurnAlreadyRunningError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message
|
||||
@@ -540,6 +490,12 @@ export function App({
|
||||
useState<number | null>(null);
|
||||
const [directCodexTransientReply, setDirectCodexTransientReply] =
|
||||
useState('');
|
||||
const directCodexTransientReplyRef = useRef('');
|
||||
const directCodexInterruptedPartialRef = useRef<{
|
||||
projectPath: string;
|
||||
text: string;
|
||||
messageId: string;
|
||||
} | null>(null);
|
||||
const [
|
||||
directCodexTransientReplyUpdatedAt,
|
||||
setDirectCodexTransientReplyUpdatedAt,
|
||||
@@ -579,6 +535,7 @@ export function App({
|
||||
setDirectCodexProgress('');
|
||||
setDirectCodexProgressUpdatedAt(null);
|
||||
setDirectCodexTransientReply('');
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
}
|
||||
|
||||
@@ -592,6 +549,7 @@ export function App({
|
||||
}
|
||||
activeDirectCodexTurnRef.current = null;
|
||||
setDirectCodexTransientReply('');
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
return true;
|
||||
}
|
||||
@@ -1232,8 +1190,6 @@ export function App({
|
||||
activeDirectCodexTurnRef.current = null;
|
||||
setDirectCodexProgress('处理失败,正在同步错误');
|
||||
setDirectCodexProgressUpdatedAt(updatedAt);
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
return;
|
||||
}
|
||||
if (payload.status === 'completed') {
|
||||
@@ -1245,6 +1201,7 @@ export function App({
|
||||
}
|
||||
if (typeof payload.accumulatedText === 'string') {
|
||||
setDirectCodexTransientReply(payload.accumulatedText);
|
||||
directCodexTransientReplyRef.current = payload.accumulatedText;
|
||||
setDirectCodexTransientReplyUpdatedAt(updatedAt);
|
||||
}
|
||||
},
|
||||
@@ -1840,9 +1797,8 @@ export function App({
|
||||
);
|
||||
if (localProjectPathRef.current === nextProjectPath) {
|
||||
// Any history read started before this terminal append may hold
|
||||
// a user-only snapshot. Invalidate it before releasing the
|
||||
// in-memory claim so that stale hydration cannot replay the
|
||||
// same billable Direct turn.
|
||||
// A stale history snapshot may still be missing this terminal
|
||||
// append. Invalidate it before releasing the in-memory claim.
|
||||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||||
}
|
||||
recoveredDirectCodexTurnClaimsRef.current.delete(claimKey);
|
||||
@@ -2698,9 +2654,6 @@ export function App({
|
||||
projectConversation.messages,
|
||||
supervisorConversation?.messages ?? [],
|
||||
);
|
||||
const unansweredDirectTurn = directCodexProductRuntime
|
||||
? unansweredDirectCodexConversationTurn(conversationMessages)
|
||||
: null;
|
||||
if (
|
||||
conversationContainsProjectSupervisorResponseStream(
|
||||
supervisorConversation?.messages ?? [],
|
||||
@@ -2752,24 +2705,6 @@ export function App({
|
||||
});
|
||||
return nextConversationMessages;
|
||||
});
|
||||
if (unansweredDirectTurn) {
|
||||
const claimKey = `${nextProjectPath}\u0000${unansweredDirectTurn.turnId}`;
|
||||
void Promise.resolve().then(() => {
|
||||
if (
|
||||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||||
localProjectPathRef.current !== nextProjectPath ||
|
||||
activeDirectCodexTurnRef.current ||
|
||||
recoveredDirectCodexTurnClaimsRef.current.has(claimKey)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
recoveredDirectCodexTurnClaimsRef.current.add(claimKey);
|
||||
void executeChatAgentReply({
|
||||
prompt: unansweredDirectTurn.prompt,
|
||||
clientTurnId: unansweredDirectTurn.turnId,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
projectSupervisorHistoryLoadVersionRef.current !== loadVersion ||
|
||||
@@ -5459,6 +5394,20 @@ export function App({
|
||||
},
|
||||
},
|
||||
);
|
||||
const persistDirectPartialMessage = (messageId: string, text: string) =>
|
||||
directInvoke<LocalConversationResult>(
|
||||
'append_local_conversation_message',
|
||||
{
|
||||
projectPath: directProjectPath,
|
||||
agentId: null,
|
||||
messageId,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `${text.trim()}\nunexpected interrupt happened here`,
|
||||
agentId: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
const recoveredDirectCodexTurnClaimKey = `${directProjectPath}\u0000${clientTurnId}`;
|
||||
recoveredDirectCodexTurnClaimsRef.current.add(
|
||||
recoveredDirectCodexTurnClaimKey,
|
||||
@@ -5473,9 +5422,21 @@ export function App({
|
||||
setDirectCodexProgress('已发送消息,正在等待陶泥儿回复');
|
||||
setDirectCodexProgressUpdatedAt(Date.now());
|
||||
setDirectCodexTransientReply('');
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
setProjectSupervisorRuntimeError('');
|
||||
try {
|
||||
const interruptedPartial = directCodexInterruptedPartialRef.current;
|
||||
if (
|
||||
interruptedPartial?.projectPath === directProjectPath &&
|
||||
interruptedPartial.text.trim()
|
||||
) {
|
||||
await persistDirectPartialMessage(
|
||||
interruptedPartial.messageId,
|
||||
interruptedPartial.text,
|
||||
);
|
||||
directCodexInterruptedPartialRef.current = null;
|
||||
}
|
||||
// Persist the original user intent and stable turn identity before
|
||||
// Codex can start any billable or externally visible work. The
|
||||
// regular conversation writer may race this call, but messageId
|
||||
@@ -5526,10 +5487,10 @@ export function App({
|
||||
}
|
||||
}
|
||||
// Rust persists a successful Direct reply before returning Ok. The
|
||||
// browser append is redundant, so the hydrated-turn claim can be
|
||||
// browser append is redundant, so the in-memory turn claim can be
|
||||
// released without reopening the Provider side effect. Invalidate
|
||||
// any user-only history snapshot captured before Rust committed the
|
||||
// terminal reply first.
|
||||
// any history snapshot captured before Rust committed the terminal
|
||||
// reply first.
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
projectSupervisorHistoryLoadVersionRef.current += 1;
|
||||
}
|
||||
@@ -5565,6 +5526,23 @@ export function App({
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
);
|
||||
const partial = directCodexTransientReplyRef.current.trim();
|
||||
if (partial) {
|
||||
const partialMessageId =
|
||||
globalThis.crypto?.randomUUID?.() ||
|
||||
`direct-partial-${Date.now().toString(36)}`;
|
||||
directCodexInterruptedPartialRef.current = {
|
||||
projectPath: directProjectPath,
|
||||
text: partial,
|
||||
messageId: partialMessageId,
|
||||
};
|
||||
try {
|
||||
await persistDirectPartialMessage(partialMessageId, partial);
|
||||
} catch {
|
||||
// The next user send retries this idempotent append before
|
||||
// constructing the replay prompt.
|
||||
}
|
||||
}
|
||||
try {
|
||||
await persistDirectAssistantMessage(visibleMessage);
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
|
||||
Reference in New Issue
Block a user