桌面主窗口和聊天子窗口可以各自建立实时收件箱连接。当前服务 router 共享一个 InboxBroker,但每个在线发送循环只发送本次 sync_rows 新生成的事件。先轮询的连接生成并取得变更事件后,另一连接再轮询相同状态会得到空列表,虽然它尚未收到的事件仍保存在共享 ring 中。因此多个连接不能保证各自收到同一待办/未读变更,连接仍正常时也可能漏掉实时回调。
证据层级:2026-09-30 仅用原 InboxBroker 和合成状态做无网络逻辑验证:第一次变更按 A→B 轮询,A 得到 seq1、B 得到空列表,B 的 ring 查询仍可取得 seq1;第二次变更反转次序,B 得到 seq2、A 得到空列表,A 的 ring 查询仍可取得 seq2。单消费者对照正常得到事件。当前 handler 的在线循环只遍历 sync_rows 返回值;events_after(cursor) 只用于初始化的一条恢复分支。没有运行真实 ASGI handler、SSE 连接、桌面多窗口或真实聊天,不将该合成结果称为生产 UI 复现。
现有保护与补偿:每连接有自己的 cursor,broker 保存事件 ring,前端有当前连接检查和 seq/event_id 去重。重新连接的 snapshot、挂载 REST 请求、当前聊天的部分事件/状态变化与执行完成后刷新可重新取得列表状态;这些路径不能保证每个持续在线的连接及时收到缺失的 inbox event,也不会由 REST 列表更新补发相应通知回调。通知的本地去重是另一层保护,不是事件广播。共享事件没有从 ring 删除,聊天、审批和用户输入的持久对象也没有被此次逻辑删除。
影响与优先级:按 P2 实时交互可靠性问题追踪。条件是同一个服务 broker 存在多个同时在线的消费者,并发生状态变更;哪一连接遗漏取决于轮询次序,不能断言后打开的窗口总会遗漏。某个窗口可暂时保留旧侧栏待办/未读状态,或缺少依赖实时回调的提示。其他刷新可能恢复列表;本次未证明不可恢复数据损失、审批执行错误或真实任务停滞。
源码定位:
apps/web/conversation_api.py:353:router closure 创建一个共享 InboxBroker。
muteki/conversation/inbox.py:174-181,184-243:ring 查询与共享 fingerprint 更新;同一状态第二次 sync 不再生成事件。
apps/web/conversation_api.py:797-819:初始化恢复分支读取 ring,在线发送仅遍历本次 emitted。
apps/web/ui/lib/useConversation.ts:1965-2001:snapshot/event 入列及回调;当前连接与去重保护存在。
apps/web/ui/components/conversation/ConversationShell.tsx:333-343,382-397,1825-1839,1853:通知回调和现有 REST 补偿触发条件。
apps/web/ui/lib/threadNotifications.ts:210-249:通知筛选与本地去重,不构成传输广播。
桌面融合方向:将共享事件生成与每连接交付分开。每个连接在同步状态后从共享 ring 按自己的 cursor 读取尚未交付的事件,而非仅使用当前 sync_rows 的返回值;完整交付后推进本连接游标。新连接的 snapshot bootstrap 也不能无条件用 seed_rows 改写已有共享 producer 的 fingerprint 基线,否则尚未生成的变更会直接消失于 ring;仅在空 broker 初始化或由统一 producer 提交状态时更新基线。保留稳定事件身份、前端去重、snapshot 水位、断线恢复和认证合同。桌面 renderer/broker 对持续在线却缺少更新的状态要有明确可观察性;共享服务传输是必要依赖,单纯改侧栏样式或增加通知开关无法补足。此次只登记方案与问题,不修改桌面、Web 或服务源码。
验收:同一 broker、同一 epoch 下建立两个合成消费者;每次变更无论 A→B 或 B→A 轮询,两个消费者都按各自游标收到一次相同 seq/event_id。变更尚未被旧连接轮询时加入新 snapshot 消费者,也不能让旧连接漏掉该变更。随后相同状态不重复回调;单消费者行为保持正确。恢复 snapshot、有限 ring 的缺口处理、认证拒绝与过期 epoch 分别保留独立验收。真实 ASGI/SSE/桌面多窗口验证只使用合成聊天,并明确区分列表补偿与实时回调交付,不用私有数据作证据。
去重:#80 是已收到的新侧栏状态被迟到 REST 结果覆盖,发生在消费者状态提交端;本条是同一 epoch 内共享生产者没有向所有连接交付事件。#81 是服务重启后旧游标与新序列的 epoch 不一致;本条没有重启、游标也未超过待发事件,仍会漏交付。#78 是事件流认证入口,与已授权连接的交付可靠性不同。两个窗口、轮询次序和不同待办类型共同归为这一条 fan-out 根因。关联 桌面聊天融合 Epic #31。
公开固定源码:共享 broker 和传输、在线发送循环、InboxBroker。这两份服务源码已与本地文件字节对照一致;前端定位以当前本地审计源码为准,不声称已测试公开发行包。
补充同一共享 producer 基线的合成时序:原纯 InboxBroker 先以旧状态 seed,服务状态变化后,新连接按原 handler 的 snapshot 分支再次 seed_rows 新状态;broker 的 seq/ring 仍为 0,旧连接随后 sync_rows 同一新状态也不产生事件。没有新 snapshot 的对照则产生 seq1。这个证据来自原 broker 方法与 handler 源码组合,未运行真实 SSE;它说明仅在在线循环补读 ring 还不够,还需防止新消费者改写共享 diff 基线。完整合成轨迹在本地 ignored inbox-snapshot-seed-fanout-result.json。
桌面主窗口和聊天子窗口可以各自建立实时收件箱连接。当前服务 router 共享一个 InboxBroker,但每个在线发送循环只发送本次
sync_rows新生成的事件。先轮询的连接生成并取得变更事件后,另一连接再轮询相同状态会得到空列表,虽然它尚未收到的事件仍保存在共享 ring 中。因此多个连接不能保证各自收到同一待办/未读变更,连接仍正常时也可能漏掉实时回调。证据层级:2026-09-30 仅用原 InboxBroker 和合成状态做无网络逻辑验证:第一次变更按 A→B 轮询,A 得到 seq1、B 得到空列表,B 的 ring 查询仍可取得 seq1;第二次变更反转次序,B 得到 seq2、A 得到空列表,A 的 ring 查询仍可取得 seq2。单消费者对照正常得到事件。当前 handler 的在线循环只遍历
sync_rows返回值;events_after(cursor)只用于初始化的一条恢复分支。没有运行真实 ASGI handler、SSE 连接、桌面多窗口或真实聊天,不将该合成结果称为生产 UI 复现。现有保护与补偿:每连接有自己的 cursor,broker 保存事件 ring,前端有当前连接检查和 seq/event_id 去重。重新连接的 snapshot、挂载 REST 请求、当前聊天的部分事件/状态变化与执行完成后刷新可重新取得列表状态;这些路径不能保证每个持续在线的连接及时收到缺失的 inbox event,也不会由 REST 列表更新补发相应通知回调。通知的本地去重是另一层保护,不是事件广播。共享事件没有从 ring 删除,聊天、审批和用户输入的持久对象也没有被此次逻辑删除。
影响与优先级:按 P2 实时交互可靠性问题追踪。条件是同一个服务 broker 存在多个同时在线的消费者,并发生状态变更;哪一连接遗漏取决于轮询次序,不能断言后打开的窗口总会遗漏。某个窗口可暂时保留旧侧栏待办/未读状态,或缺少依赖实时回调的提示。其他刷新可能恢复列表;本次未证明不可恢复数据损失、审批执行错误或真实任务停滞。
源码定位:
apps/web/conversation_api.py:353:router closure 创建一个共享 InboxBroker。muteki/conversation/inbox.py:174-181,184-243:ring 查询与共享 fingerprint 更新;同一状态第二次 sync 不再生成事件。apps/web/conversation_api.py:797-819:初始化恢复分支读取 ring,在线发送仅遍历本次 emitted。apps/web/ui/lib/useConversation.ts:1965-2001:snapshot/event 入列及回调;当前连接与去重保护存在。apps/web/ui/components/conversation/ConversationShell.tsx:333-343,382-397,1825-1839,1853:通知回调和现有 REST 补偿触发条件。apps/web/ui/lib/threadNotifications.ts:210-249:通知筛选与本地去重,不构成传输广播。桌面融合方向:将共享事件生成与每连接交付分开。每个连接在同步状态后从共享 ring 按自己的 cursor 读取尚未交付的事件,而非仅使用当前
sync_rows的返回值;完整交付后推进本连接游标。新连接的 snapshot bootstrap 也不能无条件用 seed_rows 改写已有共享 producer 的 fingerprint 基线,否则尚未生成的变更会直接消失于 ring;仅在空 broker 初始化或由统一 producer 提交状态时更新基线。保留稳定事件身份、前端去重、snapshot 水位、断线恢复和认证合同。桌面 renderer/broker 对持续在线却缺少更新的状态要有明确可观察性;共享服务传输是必要依赖,单纯改侧栏样式或增加通知开关无法补足。此次只登记方案与问题,不修改桌面、Web 或服务源码。验收:同一 broker、同一 epoch 下建立两个合成消费者;每次变更无论 A→B 或 B→A 轮询,两个消费者都按各自游标收到一次相同 seq/event_id。变更尚未被旧连接轮询时加入新 snapshot 消费者,也不能让旧连接漏掉该变更。随后相同状态不重复回调;单消费者行为保持正确。恢复 snapshot、有限 ring 的缺口处理、认证拒绝与过期 epoch 分别保留独立验收。真实 ASGI/SSE/桌面多窗口验证只使用合成聊天,并明确区分列表补偿与实时回调交付,不用私有数据作证据。
去重:#80 是已收到的新侧栏状态被迟到 REST 结果覆盖,发生在消费者状态提交端;本条是同一 epoch 内共享生产者没有向所有连接交付事件。#81 是服务重启后旧游标与新序列的 epoch 不一致;本条没有重启、游标也未超过待发事件,仍会漏交付。#78 是事件流认证入口,与已授权连接的交付可靠性不同。两个窗口、轮询次序和不同待办类型共同归为这一条 fan-out 根因。关联 桌面聊天融合 Epic #31。
公开固定源码:共享 broker 和传输、在线发送循环、InboxBroker。这两份服务源码已与本地文件字节对照一致;前端定位以当前本地审计源码为准,不声称已测试公开发行包。
补充同一共享 producer 基线的合成时序:原纯
InboxBroker先以旧状态 seed,服务状态变化后,新连接按原 handler 的 snapshot 分支再次seed_rows新状态;broker 的 seq/ring 仍为 0,旧连接随后sync_rows同一新状态也不产生事件。没有新 snapshot 的对照则产生 seq1。这个证据来自原 broker 方法与 handler 源码组合,未运行真实 SSE;它说明仅在在线循环补读 ring 还不够,还需防止新消费者改写共享 diff 基线。完整合成轨迹在本地 ignoredinbox-snapshot-seed-fanout-result.json。