feat: harden support, localize knowledge and add operator availability [skip ci]

Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
This commit is contained in:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -1,3 +1,5 @@
// Modified in the independent hardening fork; see docs/hardening/README.md.
// Modified for AgentDeskFixes: guard private state, captured recipients, and stale async completions.
"use client"
import { create } from "zustand"
@@ -104,9 +106,9 @@ type AgentConversationsStore = {
loadOlderMessages: () => Promise<void>
syncLatestMessages: (conversationId: number) => Promise<void>
markSelectedConversationRead: () => Promise<void>
sendMessage: (html: string) => Promise<AgentMessage | null>
uploadImage: (file: File) => Promise<AgentAsset | null>
sendAttachment: (file: File) => Promise<AgentMessage | null>
sendMessage: (html: string, conversationId?: number) => Promise<AgentMessage | null>
uploadImage: (file: File, conversationId?: number) => Promise<AgentAsset | null>
sendAttachment: (file: File, conversationId?: number) => Promise<AgentMessage | null>
recallMessage: (messageId: number) => Promise<AgentMessage | null>
applyRealtimeMessageCreated: (message: AgentMessage) => void
applyRealtimeAgentActivity: (payload: AgentActivityEventPayload) => void
@@ -119,6 +121,22 @@ type AgentConversationsStore = {
let conversationsRequestSeq = 0
let messagesRequestSeq = 0
let sessionGeneration = 0
function captureSessionScope() {
const generation = sessionGeneration
const userId = readSession()?.user.id
return () =>
generation === sessionGeneration && Boolean(userId) && readSession()?.user.id === userId
}
/** Invalidates every outstanding request before removing private dashboard data. */
export function resetAgentConversations() {
sessionGeneration += 1
conversationsRequestSeq += 1
messagesRequestSeq += 1
useAgentConversationsStore.setState(useAgentConversationsStore.getInitialState(), true)
}
export const useAgentConversationsStore = create<AgentConversationsStore>((set, get) => ({
searchKeyword: "",
@@ -167,6 +185,8 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
},
loadConversations: async () => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession()) return
const requestSeq = ++conversationsRequestSeq
const store = get()
@@ -180,7 +200,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
)
const conversations = ensureArray(data.results)
if (requestSeq !== conversationsRequestSeq) {
if (requestSeq !== conversationsRequestSeq || !isCurrentSession()) {
return
}
@@ -216,7 +236,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
})
}
} catch (error) {
if (requestSeq === conversationsRequestSeq) {
if (requestSeq === conversationsRequestSeq && isCurrentSession()) {
set({ conversationsLoading: false })
}
throw error
@@ -246,10 +266,14 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
},
loadMessages: async (conversationId, options = {}) => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession() || get().selectedConversationId !== conversationId) return
const requestSeq = ++messagesRequestSeq
const store = get()
const shouldShowLoading =
options.forceLoading || store.messagesLoadedConversationId !== conversationId
// A fresh snapshot supersedes any older page request in the prior generation.
set({ messagesLoadingMore: false })
if (shouldShowLoading) {
set({
@@ -270,7 +294,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
limit: 50,
})
if (requestSeq !== messagesRequestSeq) {
if (requestSeq !== messagesRequestSeq || !isCurrentSession()) {
return
}
@@ -288,7 +312,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
messagesHasMore: Boolean(data.hasMore),
})
} catch (error) {
if (requestSeq === messagesRequestSeq) {
if (requestSeq === messagesRequestSeq && isCurrentSession()) {
set({ messagesLoading: false })
}
throw error
@@ -296,6 +320,9 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
},
loadOlderMessages: async () => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession()) return
const selectionGeneration = messagesRequestSeq
const conversationId = get().selectedConversationId
if (!conversationId || get().messagesLoadingMore || !get().messagesHasMore) {
return
@@ -312,7 +339,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
cursor: cursorId,
limit: 50,
})
if (get().selectedConversationId !== conversationId) {
if (!isCurrentSession() || selectionGeneration !== messagesRequestSeq || get().selectedConversationId !== conversationId) {
return
}
const incoming = ensureArray(data.results)
@@ -328,12 +355,17 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
}
})
} catch (error) {
set({ messagesLoadingMore: false })
if (isCurrentSession() && selectionGeneration === messagesRequestSeq && get().selectedConversationId === conversationId) {
set({ messagesLoadingMore: false })
}
throw error
}
},
syncLatestMessages: async (conversationId) => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession()) return
const selectionGeneration = messagesRequestSeq
if (conversationId <= 0) {
return
}
@@ -342,7 +374,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
conversationId,
limit: 50,
})
if (get().selectedConversationId !== conversationId) {
if (!isCurrentSession() || selectionGeneration !== messagesRequestSeq || get().selectedConversationId !== conversationId) {
return
}
const batch = ensureArray(data.results)
@@ -370,6 +402,8 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
},
markSelectedConversationRead: async () => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession()) return
const store = get()
const conversationId = store.selectedConversationId
const conversation = store.conversations.find((item) => item.id === conversationId)
@@ -390,6 +424,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
set({ readingMessageId: lastMessage.id })
try {
await markAgentMessageRead(conversationId, lastMessage.id)
if (!isCurrentSession() || get().readingMessageId !== lastMessage.id) return
set((current) => {
if (current.selectedConversationId !== conversationId) {
return { readingMessageId: 0 }
@@ -414,7 +449,9 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
}
})
} catch (error) {
set({ readingMessageId: 0 })
if (isCurrentSession() && get().readingMessageId === lastMessage.id) {
set({ readingMessageId: 0 })
}
throw error
}
},
@@ -513,7 +550,10 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
},
resyncRealtimeData: async (conversationId) => {
const isCurrentSession = captureSessionScope()
if (!isCurrentSession()) return
await get().loadConversations()
if (!isCurrentSession()) return
const selectedConversationId = get().selectedConversationId
const targetConversationId = conversationId ?? selectedConversationId
if (targetConversationId && selectedConversationId === targetConversationId) {
@@ -524,10 +564,15 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
}
},
sendMessage: async (html) => {
sendMessage: async (html, recipientId) => {
const isCurrentSession = captureSessionScope()
const trimmedContent = html.trim()
const { selectedConversationId, sending } = get()
if (!selectedConversationId || !trimmedContent || sending) {
if (
!isCurrentSession() || !selectedConversationId ||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
!trimmedContent || sending || get().uploadingAsset
) {
return null
}
@@ -540,7 +585,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
clientMsgId: `agent_${generateUUID()}`,
})
if (get().selectedConversationId === selectedConversationId) {
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
set((current) => ({
messages: current.messages.some((m) => m.id === message.id)
? current.messages.map((m) => (m.id === message.id ? message : m))
@@ -561,33 +606,45 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
return message
} finally {
set({ sending: false })
if (isCurrentSession()) set({ sending: false })
}
},
uploadImage: async (file) => {
uploadImage: async (file, recipientId) => {
const isCurrentSession = captureSessionScope()
const { selectedConversationId, sending, uploadingAsset } = get()
if (!selectedConversationId || sending || uploadingAsset) {
if (
!isCurrentSession() || !selectedConversationId ||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
sending || uploadingAsset
) {
return null
}
set({ uploadingAsset: true })
try {
return await uploadAgentConversationImage(selectedConversationId, file)
const asset = await uploadAgentConversationImage(selectedConversationId, file)
return isCurrentSession() ? asset : null
} finally {
set({ uploadingAsset: false })
if (isCurrentSession()) set({ uploadingAsset: false })
}
},
sendAttachment: async (file) => {
sendAttachment: async (file, recipientId) => {
const isCurrentSession = captureSessionScope()
const { selectedConversationId, sending, uploadingAsset } = get()
if (!selectedConversationId || sending || uploadingAsset) {
if (
!isCurrentSession() || !selectedConversationId ||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
sending || uploadingAsset
) {
return null
}
set({ uploadingAsset: true })
try {
const asset = await uploadAgentConversationAttachment(selectedConversationId, file)
if (!isCurrentSession()) return null
const message = await sendAgentMessage({
conversationId: selectedConversationId,
messageType: "attachment",
@@ -596,7 +653,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
clientMsgId: `agent_attachment_${generateUUID()}`,
})
if (get().selectedConversationId === selectedConversationId) {
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
set((current) => ({
messages: current.messages.some((m) => m.id === message.id)
? current.messages.map((m) => (m.id === message.id ? message : m))
@@ -617,20 +674,22 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
return message
} finally {
set({ uploadingAsset: false })
if (isCurrentSession()) set({ uploadingAsset: false })
}
},
recallMessage: async (messageId) => {
const isCurrentSession = captureSessionScope()
const { selectedConversationId, recallingMessageId } = get()
if (!selectedConversationId || messageId <= 0 || recallingMessageId === messageId) {
if (!isCurrentSession() || !selectedConversationId || messageId <= 0 || recallingMessageId === messageId) {
return null
}
set({ recallingMessageId: messageId })
try {
const message = await recallAgentMessage(messageId)
if (get().selectedConversationId === selectedConversationId) {
if (!isCurrentSession()) return null
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
set((current) => {
const nextMessages = current.messages.map((item) =>
item.id === message.id ? message : item
@@ -656,11 +715,11 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
}
})
} else {
set({ recallingMessageId: 0 })
if (isCurrentSession() && get().recallingMessageId === messageId) set({ recallingMessageId: 0 })
}
return message
} catch (error) {
set({ recallingMessageId: 0 })
if (isCurrentSession() && get().recallingMessageId === messageId) set({ recallingMessageId: 0 })
throw error
}
},