-
Notifications
You must be signed in to change notification settings - Fork 1
🐛 fix(web): 修复 runtime events 内存无界增长 #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
2b876dd
fc3cf31
b5867ee
9cac260
2c99abd
8dd923a
bec3ecc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,14 @@ import { | |
| activeTabIdAtom, | ||
| archiveInitialViewAtom, | ||
| currentWorkspaceIdAtom, | ||
| agentRuntimeEventsAtom, | ||
| settingsInitialTabAtom, | ||
| sidebarCollapsedAtom, | ||
| tabsAtom, | ||
| workspacePinnedIdsAtom, | ||
| } from '@/atoms' | ||
| import { removeRuntimeEvents } from '@/hooks/runtime-event-state' | ||
| import { threadMessagesCache } from '@/components/agent/thread-messages-cache' | ||
| import { CreateWorkspaceDialog } from '@/components/workspace/CreateWorkspaceDialog' | ||
| import { ConfirmDialog } from '@/components/ui/confirm-dialog' | ||
| import { | ||
|
|
@@ -103,6 +106,7 @@ export function LeftSidebar({ forceCollapsed = false }: { forceCollapsed?: boole | |
| const [workspaces, setWorkspaces] = useAtom(agentWorkspacesAtom) | ||
| const [pinnedIds, setPinnedIds] = useAtom(workspacePinnedIdsAtom) | ||
| const setSettingsInitialTab = useSetAtom(settingsInitialTabAtom) | ||
| const setRuntimeEvents = useSetAtom(agentRuntimeEventsAtom) | ||
| const setArchiveInitialView = useSetAtom(archiveInitialViewAtom) | ||
| const [expandedWorkspaceIds, setExpandedWorkspaceIds] = useState<string[]>([]) | ||
| const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false) | ||
|
|
@@ -362,6 +366,8 @@ export function LeftSidebar({ forceCollapsed = false }: { forceCollapsed?: boole | |
| try { | ||
| await sidecarCall(AGENT_IPC_CHANNELS.TRASH_THREAD, { threadId: thread.id }) | ||
| removeThreadFromNavigation(thread.id) | ||
| setRuntimeEvents((prev) => removeRuntimeEvents(prev, thread.id)) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 对仍在流式运行的线程,这条清理会被后续事件抵消。 sidecar 的 |
||
| threadMessagesCache.invalidate(thread.id) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 两个 TRASH_THREAD 路径的清理子集漂移:ArchiveSettings.handleTrash(:44-45)还会
|
||
| toast.success('已移入回收站') | ||
| } catch (error) { | ||
| console.error('[LeftSidebar] 移入回收站失败:', error) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,9 @@ import { toast } from 'sonner' | |
| import type { AgentThreadMeta } from '@lume/shared' | ||
| import { AGENT_IPC_CHANNELS } from '@lume/shared' | ||
| import { cn } from '@/lib/utils' | ||
| import { agentInputDraftAtom, agentInputHistoryAtom, agentWorkspacesAtom } from '@/atoms' | ||
| import { agentInputDraftAtom, agentInputHistoryAtom, agentRuntimeEventsAtom, agentWorkspacesAtom } from '@/atoms' | ||
| import { removeRuntimeEvents } from '@/hooks/runtime-event-state' | ||
| import { threadMessagesCache } from '@/components/agent/thread-messages-cache' | ||
| import { sidecarCall } from '@/lib/desktop-api' | ||
| import { removeDraft, removeHistory } from '@/lib/agent-input-draft-state' | ||
| import { ConfirmDialog } from '@/components/ui/confirm-dialog' | ||
|
|
@@ -36,12 +38,15 @@ export function ArchiveSettings({ initialView }: { initialView?: 'archive' | 'tr | |
| const workspaces = useAtomValue(agentWorkspacesAtom) | ||
| const setDraftState = useSetAtom(agentInputDraftAtom) | ||
| const setHistoryState = useSetAtom(agentInputHistoryAtom) | ||
| const setRuntimeEvents = useSetAtom(agentRuntimeEventsAtom) | ||
| const removeThreadInputState = React.useCallback( | ||
| (threadId: string) => { | ||
| setDraftState((prev) => removeDraft(prev, threadId)) | ||
| setHistoryState((prev) => removeHistory(prev, threadId)) | ||
| setRuntimeEvents((prev) => removeRuntimeEvents(prev, threadId)) | ||
| threadMessagesCache.invalidate(threadId) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 建议把这套清理抽成共享 chokepoint(如 本 PR 在 ArchiveSettings 组了 draft+history+events+cache 四件套,又在 LeftSidebar 手拼了不同的两件套,而线程移除实际有 6 个向量:归档、工作区 deleteLumeData 级联、项目删除级联、WelcomeView 删除重试线程、数据管理页清空回收站、sidecar 启动时 30 天自动清理——后四者完全没接清理(详见整体评论)。同类的 |
||
| }, | ||
| [setDraftState, setHistoryState], | ||
| [setDraftState, setHistoryState, setRuntimeEvents], | ||
| ) | ||
| const [view, setView] = React.useState<View>(initialView ?? 'archive') | ||
| const [archivedThreads, setArchivedThreads] = React.useState<AgentThreadMeta[]>([]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,7 +74,9 @@ export function hydrateRuntimeEvents( | |
| if (result.events.length === 0) { | ||
| return prev | ||
| } | ||
| const events = mergeHydratedRuntimeEvents(result.events, current?.events ?? []) | ||
| // 与 append 路径同上限:sidecar 回放不封顶,hydrate 不 trim 会让重开的超长线程 | ||
| // 全量驻留内存(且直到下一条 append 前都无界)。trim 规则同 append:先丢头部 delta。 | ||
| const events = trimRuntimeEvents(mergeHydratedRuntimeEvents(result.events, current?.events ?? [])) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 数据丢失风险:hydrate 新增的 trim 会永久丢失头部 turn 的助手正文。
后果:超过 2000 条回放事件的长线程重开(或
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 follow-up 标记:渲染端 trim 只封内存,不封 IPC 载荷。
|
||
| if (current && sameRuntimeEvents(current.events, events)) { | ||
| return prev | ||
| } | ||
|
|
@@ -91,6 +93,17 @@ export function hydrateRuntimeEvents( | |
| } | ||
| } | ||
|
|
||
| /** 删除线程的 runtime events 条目(回收站/永久删除时调用,防止 Record 只增不减)。 */ | ||
| export function removeRuntimeEvents( | ||
| prev: RuntimeEventState, | ||
| threadId: string, | ||
| ): RuntimeEventState { | ||
| if (!(threadId in prev)) return prev | ||
| const next = { ...prev } | ||
| delete next[threadId] | ||
| return next | ||
| } | ||
|
|
||
| function mergeHydratedRuntimeEvents( | ||
| persistedEvents: LumeRuntimeEvent[], | ||
| liveEvents: LumeRuntimeEvent[], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 相邻的
archiveThread(:334-352,同文件、同一菜单)没有接入这两行清理——归档路径上 Record 仍只增不减。归档是完成会话的主要收尾动作(回收站是给不要的线程用的):长会话中归档 N 个线程,每个至多 2000 条事件对象 +
threadMessagesCache条目常驻内存直到重启。清掉并无副作用——归档线程恢复/重新打开时会走AgentMessages.tsx:243重新 hydrate,hydrateRuntimeEvents也会从持久化事件重建terminalStatus。建议本 PR 一并补上,或抽一个共享清理助手(见 ArchiveSettings 侧评论)。