Compare commits

...

2 Commits

Author SHA1 Message Date
ONLY-yours a823410bba 🐛 fix(gateway): guard against stale operation after token refresh
Re-check the topic's running operationId after the async token-refresh
await. A newer executeGatewayAgent call may have taken over for the
same topic during that wait, which would cause two concurrent streams.
Bail early if the operationId no longer matches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:38:30 +08:00
ONLY-yours fbda59ff28 🐛 fix(agent-builder): explicitly sync editing agent ID to chatStore
The Agent Builder reads the wrong agent's context because
`getChatStoreState().activeAgentId` — which the chat service uses to
build `agentBuilderContext` — can drift from the agent currently open in
the profile editor under certain timing conditions (SWR cache hits,
navigation order, React effect scheduling).

Fix: `AgentBuilderProvider` now accepts an `editingAgentId` prop and
writes it to `chatStore.activeAgentId` in a `useEffect`. This makes
the data flow explicit instead of relying on `AgentIdSync` alone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:12:52 +08:00
3 changed files with 65 additions and 37 deletions
@@ -1,5 +1,5 @@
import { type ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { memo, useEffect, useMemo } from 'react';
import { ConversationProvider } from '@/features/Conversation';
import { useOperationState } from '@/hooks/useOperationState';
@@ -10,6 +10,12 @@ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
interface AgentBuilderProviderProps {
agentId: string;
children: ReactNode;
/**
* The ID of the agent currently being edited in the profile page.
* This is synced to chatStore.activeAgentId so the chat service can read it
* when building the agentBuilderContext for injection into the LLM prompt.
*/
editingAgentId?: string;
}
/**
@@ -17,45 +23,60 @@ interface AgentBuilderProviderProps {
* Provides context for the Agent Builder chat panel
* Uses 'agent_builder' scope to isolate messages from main conversation
*/
const AgentBuilderProvider = memo<AgentBuilderProviderProps>(({ agentId, children }) => {
const activeTopicId = useChatStore((s) => s.activeTopicId);
const AgentBuilderProvider = memo<AgentBuilderProviderProps>(
({ agentId, editingAgentId, children }) => {
const activeTopicId = useChatStore((s) => s.activeTopicId);
// Build conversation context for agent builder
// Using agent_builder scope for message management
const context = useMemo<MessageMapKeyInput>(
() => ({
agentId,
scope: 'agent_builder',
topicId: activeTopicId,
}),
[agentId, activeTopicId],
);
// Keep chatStore.activeAgentId in sync with the agent being edited.
// The chat service reads getChatStoreState().activeAgentId to build agentBuilderContext,
// so this must reflect the profile page agent, not the Agent Builder builtin agent.
useEffect(() => {
if (!editingAgentId) return;
if (useChatStore.getState().activeAgentId === editingAgentId) return;
useChatStore.setState(
{ activeAgentId: editingAgentId },
false,
'AgentBuilderProvider/syncEditingAgentId',
);
}, [editingAgentId]);
// Get messages from ChatStore based on context
const chatKey = useMemo(
() => (context ? messageMapKey(context) : null),
[context?.agentId, context?.topicId],
);
// Build conversation context for agent builder
// Using agent_builder scope for message management
const context = useMemo<MessageMapKeyInput>(
() => ({
agentId,
scope: 'agent_builder',
topicId: activeTopicId,
}),
[agentId, activeTopicId],
);
const replaceMessages = useChatStore((s) => s.replaceMessages);
const messages = useChatStore((s) => (chatKey ? s.dbMessagesMap[chatKey] : undefined));
// Get messages from ChatStore based on context
const chatKey = useMemo(
() => (context ? messageMapKey(context) : null),
[context?.agentId, context?.topicId],
);
// Get operation state for reactive updates
const operationState = useOperationState(context);
const replaceMessages = useChatStore((s) => s.replaceMessages);
const messages = useChatStore((s) => (chatKey ? s.dbMessagesMap[chatKey] : undefined));
return (
<ConversationProvider
context={context}
hasInitMessages={!!messages}
messages={messages}
operationState={operationState}
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
>
{children}
</ConversationProvider>
);
});
// Get operation state for reactive updates
const operationState = useOperationState(context);
return (
<ConversationProvider
context={context}
hasInitMessages={!!messages}
messages={messages}
operationState={operationState}
onMessagesChange={(msgs, ctx) => {
replaceMessages(msgs, { context: ctx });
}}
>
{children}
</ConversationProvider>
);
},
);
export default AgentBuilderProvider;
+1 -1
View File
@@ -39,7 +39,7 @@ const AgentBuilder = memo(() => {
}}
>
{agentId && agentBuilderId ? (
<AgentBuilderProvider agentId={agentBuilderId}>
<AgentBuilderProvider agentId={agentBuilderId} editingAgentId={agentId}>
<AgentBuilderConversation agentId={agentBuilderId} />
</AgentBuilderProvider>
) : (
@@ -533,6 +533,13 @@ export class GatewayActionImpl {
// Get a fresh JWT token (original expired after 5 min)
const { token } = await aiAgentService.refreshGatewayToken(topicId);
// Re-check after the async token refresh: a newer executeGatewayAgent call may have
// taken over for this topic while we were waiting. If so, bail to avoid a duplicate stream.
// (disconnectFromGateway on the stale op is a no-op here because we haven't connected yet.)
const topicCurrentOpId = topicSelectors.getTopicById(topicId)(this.#get())?.metadata
?.runningOperation?.operationId;
if (topicCurrentOpId && topicCurrentOpId !== operationId) return;
const agentId = this.#get().activeAgentId;
const context = {
agentId,