尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CherryHQ Cherry Studio Steer 队列状态机整合:以 `stream.status` 为单一权威的设计重构

CherryHQ Cherry Studio Steer 队列状态机整合:以 `stream.status` 为单一权威的设计重构 CherryHQ Cherry Studio Steer 队列状态机整合以stream.status为单一权威的设计重构【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本文是 CherryHQ/cherry-studio 主进程 AI 流式管线AiStreamManager一次设计级重构的技术指南。它围绕 steer-state-machine-consolidation.md 展开讲清楚对话中途追加提问steer在运行中、已结束、等待审批三种时刻如何做继续、排队、丢弃决策以及为什么必须删除手工维护的影子状态lastTerminalKind、改用已解析的stream.status作为唯一权威。读完本文你将掌握该状态机的信号模型、三类竞态缺陷的成因与修复方案、与工具审批门approve-gate的串行化关系以及对应测试用例的验证方法。背景steer 队列在同一问题上做了三次不同信号的决策在 Cherry Studio 的聊天场景中当一轮生成turn仍在运行时用户在输入框继续发送消息主进程不会打断当前生成而是把这条消息持久化为用户行并放入 steer 队列pendingSteers等当前 turn 让出后在下一个步骤边界由onExecutionDone链式启动steer-continuation继续回答。这是 dispatch.ts 中dispatchStreamRequest的注入式 steer路径prepareDispatch返回pendingSteerUserMessageId后主进程调用manager.enqueuePendingSteer(...)入队。问题在于这个 steer 现在该启动延续、等待还是被丢弃这一同一个决策在代码里被放在了三个不同时刻执行且每个时刻读取的信号各不相同当前 turn 收尾时 ——onExecutionDone的链式启动门槛chaining gate收尾之后才落地的 steer ——enqueuePendingSteer生成中或结束后落地的审批响应 —— 工具审批门approve-gate位于AiService.respondToolApproval。其中有一个信号lastTerminalKind是手工维护的影子状态对 topic 终态进行投影它在结构上有两处错误最终演化出三个独立 bug评审项 1/2/3。三个信号的真相谁才是权威设计文档用一张表刻画了重构前的信号格局结合当前源码可以逐条印证信号真正的权威写入点重构前读取点漏洞lastTerminalKind: Maptopic, done\|aborted\|error否影子状态abort、clean-done、若干终态钩子enqueuePendingSteer链式收尾与审批驻留approval-park收尾时不写→ 读侧拿到undefined与aborted/error同等对待 →直接丢弃stream.status经resolveTerminalStatus→computeTopicStatus解析是每个收尾钩子多处已经存在于宽限期内的ActiveStream上runTerminalLifecycle延迟约 30 秒清理且能正确解析多模型混合终态topicDone !isLiveStatus(status)派生值过松—chaining gate对error/aborted/awaiting-approval同样为true无法区分干净完成与失败关键定义在 AiStreamManager.ts/** pending covers the pre-first-chunk window — dont compare against streaming alone. */ function isLiveStatus(status: ActiveStream[status]): boolean { return status pending || status streaming }也就是说done | aborted | error | awaiting-approval全部属于非 live 状态因此收尾后的stream.status本身就是一个精确的四路终态lastTerminalKind只是它的有损副本。三个 buglate-steer 静默丢弃、审批门绕过派发锁、多模型终态随收尾顺序翻转Item 1 —— 迟到的 steer 被静默丢弃变体 A/B/C一个在prepareDispatch期间持久化、却在 turn 收尾之后才进入enqueuePendingSteer的 steer执行循环的终态钩子并不持有派发锁在链式收尾和审批驻留收尾两种路径上lastTerminalKind都是undefined于是读取侧把它当作aborted/error丢弃——尽管dispatch的响应早已告诉渲染端成功。变体包括变体 A链式窗口内迟到的 steer变体 B审批驻留后迟到的 steer与队列等待审批后延续的设计自相矛盾变体 C延续启动窗口内的迟到 steer。Item 2 —— 审批门绕过了派发锁TOCTOUAiService.respondToolApproval原先在 per-topicdispatchLock之外快照hasLiveStream。若此时一个并发提交拿到锁并启动了一轮 live turn审批的continue-conversation会排在锁后把 anchor 翻成pending随后send()走注入分支丢弃其 models——被批准的工具永远不执行行卡在pending却仍返回{ ok: true }。这与审批门当初要堵住的漏洞一模一样只是换成了锁的缝隙。Item 3 —— 多模型混合终态随收尾顺序翻转Exec A 报错、Exec B 最后干净完成resolveTerminalStatus将stream.status解析为error但 chaining gate 只认topicDone为true且终态钩子把lastTerminalKind记录成done于是队列中的 steer链到了一个已经 errored 的 topic 上反过来干净先收尾、报错后收尾整个队列又被丢弃。同一结果、两种行为完全由哪个 exec 最后收尾决定。附带问题Item 4 / S2 / S5Item 4链式延续在模型不变时复用上一轮的executionId渲染端useExecutionOverlay/TopicStreamSubscription的终态回放映射与读取器发生键冲突把延续的 live 分块丢弃S2exec.awaitingApproval是单个布尔不按toolCallId键控兄弟工具的输出会在审批中途把它清掉S5lastTerminalKind对一次性 prompt 流也写入且只在同 idsend()时清理 → 无界增长。为什么这是重构而不是三个补丁每个窗口都能靠再多记一个 kind修好——但信号依然是必须在所有路径写入、并以有歧义的undefined读取的影子undefined到底是干净但还没记录还是不干净。这正是评审所描述的修好一个窗口又打开下一个窗口的补丁跑步机。持久化的修复是移除影子。目标设计单一权威 纯投影核心不变量已收尾的聊天流会滞留约 30 秒runTerminalLifecycle延迟清理因此activeStreams.get(topicId)?.status在整个 steer 竞态窗口内都是权威终态无流 空闲/全新 topic此时 steer 实为新的一轮。设计四步走删除lastTerminalKind顺带删除 S5 的内存泄漏chaining gate 改为基于stream.status不再用topicDoneenqueuePendingSteer变成权威状态的纯函数审批门串行化到dispatchLock之下。Chaining gatestream.status done而非topicDone// onExecutionDone, after stream.status resolveTerminalStatus(stream) const chatChaining stream.status done this.hasPendingSteer(topicId)awaiting-approval与error/aborted因为不是done而被排除——所以多模型混合错误item 3无论收尾顺序如何都不会链式启动。原先单独的approvalPending重算随之消失它不过是status awaiting-approval。enqueuePendingSteer权威状态的纯函数const prev this.activeStreams.get(topicId) if (prev isLiveStatus(prev.status)) { this.appendPendingSteer(...); return } // yields chains switch (prev?.status) { case undefined: // idle/fresh topic — no recent turn case done: this.appendPendingSteer(...); this.scheduleNextChatTurn(topicId); break // items 1A/1C case awaiting-approval: this.appendPendingSteer(...); break // queued; post-approval continuation drains it (item 1B) case aborted: case error: /* drop: log, row stays resendable */ break }undefined/done→ 排空链式/延续窗口现在读到的是done或 live 流永远不会是undefinedawaiting-approval→ 只排队不调度aborted/error→ 丢弃持久化的用户行仍可重发。不再存在歧义状态。审批门串行化到dispatchLock审批响应处理器获取已有的 per-topicdispatchLock并在锁内重新检查 liveness使 Approve 与并发提交无法交错item 2。这是审批文档中 Phase 3单一权威写者 单一串行化点在 steer 侧的体现应当一起实施。链式延续使用 turn 唯一executionId每次派发的 turn 铸造新的executionId从新占位messageId或 per-topic turn 计数器派生而非在模型不变时复用上一轮 id。渲染端的#terminalByExecutionId回放映射与useExecutionOverlay的读取键因此在链式轮次之间永不冲突——从源头修复而不是在 overlay 上打特判。若保留原 id 而用executionId anchorMessageId键控回放/读取器则记为渲染端切片的交接事项。折叠项S2把单个exec.awaitingApproval布尔替换为 execution 上的 pending-approvaltoolCallId集合chaining gate、approve-gate 与 2 小时空闲重挂都读它兄弟工具的输出便无法清除仍 pending 的审批。当前仓库中的落地验证设计文档状态为 blockers 1–3 implemented在 AiStreamManager.ts 中可以逐条核对enqueuePendingSteerL1070-L1101已完全按权威状态决策live → 入队返回aborted/error→ 记日志丢弃行保持可重发其余含undefined/done→ 入队且仅当非awaiting-approval时调用scheduleNextChatTurn。注释明确写道Decide from the single authority — the resolvedstatuson the still-in-grace stream — not a separate shadow flag。Chaining gateL1389正是const chatChaining stream.status done this.hasPendingSteer(topicId)且非链式终态路径在error/aborted时执行dropPendingSteers干净done或审批驻留则保留队列。终态解析L2080-L2105resolveTerminalStatus先由computeTopicStatus聚合各 execution 状态有 streaming → pending/streaming全部 aborted → aborted有 error → error否则 done再在有pendingApprovalToolCallIds时提升为awaiting-approval。S2 已落地onChunk对tool-approval-request以exec.pendingApprovalToolCallIds ?? new Set()按toolCallId累积L1262-L1268resolveToolApproval按toolCallId删除。Item 2 的锁内修复respondToolApproval保留廉价的hasLiveStream预检查AiService.ts L448拒绝常见竞态真正的 TOCTOU 窗口由dispatch在锁内执行prepareDispatch → send时以send()抛错封死见 dispatch.ts 的注释handler 捕获后返回{ ok: false }渲染端重置审批卡片而不是卡在提交中L513-L532。Item 4 现状toActiveExecution将executionId派生为exec.modelIdL219-L226从源码结构看同模型链式延续确实会复用同一executionId印证了该问题被划给渲染端切片键控改为executionId anchorMessageId或铸新 id的原因。测试用例对照AiStreamManager.test.ts 中的用例与设计文档的 Validation 清单一一对应变体 AL2385-L2403先入队 s0、干净收尾触发链式status 翻为done再在链式窗口内入队 s1——断言 s1 被保留等待下一次排空而非丢弃变体 BL2405-L2422tool-approval-request分块后收尾驻留为awaiting-approval迟到 steer 只排队不启动等待 Approve 派发的延续来排空多模型混合错误L2424-L2452两种收尾顺序error 先/干净后、干净先/error 后都断言不链式启动、队列被丢弃注入拒绝L616send()拒绝把准备好的 turn 注入到 live topic 上approval continue-conversation 竞态。不变量清单重构后必须始终成立的四条不变量也是回归测试的断言来源一个排队/迟到的 steer要么被恰好回答一次要么保持可重发——绝不在干净或审批驻留收尾时被静默丢弃绝不链式挂到 aborted/errored topic 上链式/丢弃决策与多模型收尾顺序无关Approve 绝不注入丢弃一个 live 延续通过dispatchLock串行化链式延续的分块实时渲染turn 唯一executionId而不是等终态 DB 刷新后才出现。影响范围与实施顺序本次改动集中在三处AiStreamManageronExecutionDone/onExecutionPaused/onExecutionError、enqueuePendingSteer、executionId铸造、删除lastTerminalKind、AiService的 approve-gatedispatchLock以及 per-execution 的 pending-approval 集合。Item 4 的渲染端修复唯一executionId要么在此落地要么作为显式渲染端切片交接。整体应与其姊妹设计 tool-approval-state-consolidation.md 的 Phase 3 同步推进——两者共享dispatchLock与单一写者规则。对状态机更宏观的背景可继续阅读 docs/references/ai/stream-manager.md。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表