Skip to content

[bug] 群聊出站回复广播失败即永久丢失(无重试通道 / 游标不回滚 / 占位半状态)——附设计提案 #40

Description

@WuFenG-Hub

现象

编排器的群聊回复在广播失败后直接丢弃cognitiveOrchestrator.ts:818-824try { await broadcastGroupChat(...) } catch { console.error('[Orchestrator] Broadcast failed:', ...); return }。回复文本、失败原因、以及"欠哪条消息一条回复"三者都不落任何表——git grep "INSERT\|UPDATE " -- src/main/services/cognitiveOrchestrator.ts 只有 4 处 UPDATE group_chat_tasks零 INSERT

一次瞬时故障(费率抖动 / 网关 5xx / Metalet 断连)= 一条回复凭空消失,用户与对端都无从知晓,机器人也不会再补。

三条实测事实(均以 upstream/main = 5026b235 为基线,非工作树)

  1. 触发消息的线索被同一个 tick 吞掉 [已证实]:失败分支在 :823 早退后不会回滚游标,tick() 继续走到 :991-994last_processed_msg_id = maxProcessedId;下次 tick 的 id > last_processed_msg_id 再也选不中那条消息。失败与成功在游标层面不可区分
  2. 占位消息会先上链,形成半状态 [已证实]:skill 特权路径在正文之前先广播一条 copyRespondingPlaceholder():697)。正文若失败,链上已有一条占位——用户看到的是"回了但没回全",比直接沉默更难诊断。
  3. 接口层把 ACK 的路断了 [已证实]BroadcastGroupChatFn 返回 Promise<void>:124-129),实现体丢掉了 sendGroupChatMessage 本可返回的 pinIdgroupChatTransport.ts:300)。没有 pinId 就没有可回读的 ACK

为什么现有机制救不了它

  • privateChatBackfillService / groupChatBackfillService 都是入站历史补拉(把别人的历史拉回本地),树内没有任何代码路径能把"本机一条发送失败、待投递的回复"重新广播出去。
  • groupSendLedger(R4 单发去重)故意进程内不持久化(注释:turn 不会跨重启),只回答"本轮是否已发过"。
  • metabot_chain_writes 也接不住它:群聊发送被双重排除——路径 /protocols/simplegroupchatchainWriteLedger.ts:29-35)与 origin internal:group-chat:41-49)都在排除集内。

设计提案(若方向被接受,我出 PR)

核心是把"发出去"当成一条有终态的持久义务,而不是一次 fire-and-forget:

  • 义务(metabot_id, group_id, idempotency_key, trigger_msg_id, state, attempts, last_error, pin_id?)
  • 状态PENDING → SUBMITTED → CONFIRMED / ABANDONEDcreatePin 返回的 pinId 只标 SUBMITTED(只证明本机写入),只有外部回读命中才 CONFIRMED
  • 幂等键可直接算出:群聊加密是 AES-CBC + 固定 IV 0000000000000000 + key=群号前 16 字符(metaWebCrypto.ts:30,注意该函数名为 encryptGroupMessageECB 但实际是 CBC)⇒ 同 (content, groupId) 密文逐字节确定k = sha256(ciphertext_hex) 可在发送前算好,且重复投递在链上公开可见。
  • 终止判据不另造口径:直接沿用树内现成的多源判据 verifyPinSources()groupTaskDaemon.ts:9151-9180):verified = found && !notFound,即至少一源命中且无任何源硬 404;主源 getPinData、副源 metafile-indexer。单源 404 在索引滞后期不算"不存在"
  • 不重造骨架agentGame/runtime.ts:380-420 retryPendingWrite() 是树内唯一「写前登记意图 + 外部回显终止」的先例(退避 [2s,5s,15s,30s]),agent_game_write_logUNIQUE (group_id, action_seq, event_id) 正是义务表的现成式样。

一条必须写进实现的硬约束

chainWriteBudget.ts:26-31 的报错文案已经把原则写清楚了:

"the underlying on-chain operation was abandoned, NOT cancelled — it may still land. Do NOT blindly re-send the same content: first verify whether it arrived (group log / on-chain lookup), then retry only if it is genuinely missing."

我们自己在私信侧刚吃过反面的教训:投递层重试没有 ACK 终止条件,导致同一段 1501 字节的内容在 40 分钟内被逐字重复上链 5 次、对端四次投诉。所以本设计里:SUBMITTED 后的冷却期必须长于实测索引延迟,否则"还没被确认"会被误判成"发送失败"而重发,精确重演那个事故。冷却期要用实测值定成显式常量 + 断言,不做魔法数字。

想确认的一件事

这个方向上游是否接受?接受的话我按上面的边界出 PR(含「单源 404 不算不存在」与「冷却期内禁止重发」两组回归用例);如果不接受、或你们已有别的计划,我就不占用 review 资源。

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions