Skip to content

[Desktop/Chat][P2] 多个实时收件箱连接应按各自游标补发待办更新 #82

Description

@jiayuqi7813

桌面主窗口和聊天子窗口可以各自建立实时收件箱连接。当前服务 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。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions