Discord 异步入站 Worker 计划

目标

将 Discord 监听器超时从用户可见的失败模式中移除,方法是让入站 Discord 回合异步化:

  1. Gateway 监听器快速接受和规范化入站事件。
  2. Discord 运行队列按现有排序边界存储序列化任务。
  3. Worker 在 Carbon 监听器生命周期之外执行实际的 agent 回合。
  4. 运行完成后将回复投递回原始频道或线程。

这是排队的 Discord 运行在 channels.discord.eventQueue.listenerTimeout 超时而 agent 运行本身仍在正常进行时的长期修复方案。

当前状态

本计划已部分实现。

已完成:

  • Discord 监听器超时和 Discord 运行超时现在是独立设置。
  • 被接受的入站 Discord 回合入队到 src/discord/monitor/inbound-worker.ts
  • Worker 现在拥有长时间运行的回合,而非 Carbon 监听器。
  • 现有的按路由排序保持不变。
  • 存在 Discord worker 路径的超时回归覆盖。

通俗地说:

  • 生产超时 bug 已修复
  • 长时间运行的回合不再因为 Discord 监听器预算到期就死掉
  • worker 架构还没完成

仍然缺少的:

  • DiscordInboundJob 仍然只是部分规范化,仍携带活跃运行时引用
  • 命令语义(stopnewreset、未来会话控制)还未完全 worker 原生
  • worker 可观测性和运维状态仍然很少
  • 没有重启持久性

为什么要做

当前行为将完整的 agent 回合绑定到监听器生命周期:

  • src/discord/monitor/listeners.ts 应用超时和中止边界。
  • src/discord/monitor/message-handler.ts 在该边界内保持排队运行。
  • src/discord/monitor/message-handler.process.ts 内联执行媒体加载、路由、dispatch、typing、草稿流式和最终回复投递。

该架构有两个坏特性:

  • 长但健康的回合可以被监听器看门狗中止
  • 即使下游运行时本会产生回复,用户也可能看不到回复

提高超时有帮助但不改变失败模式。

不做的事

  • 不在这一轮重新设计非 Discord 频道。
  • 不在第一次实现中扩展为通用的全频道 worker 框架。
  • 暂不提取共享的跨频道入站 worker 抽象;只在重复明显时共享底层原语。
  • 第一轮不添加持久崩溃恢复,除非安全着陆需要。
  • 不改变路由选择、绑定语义或 ACP 策略。

当前约束

当前 Discord 处理路径仍依赖一些不应留在长期 job 载荷中的活跃运行时对象:

  • Carbon Client
  • 原始 Discord 事件形状
  • 内存中的 guild 历史映射
  • 线程绑定管理器回调
  • 活跃的 typing 和草稿流状态

已经将执行移到了 worker 队列上,但规范化边界仍不完整。现在 worker 是”稍后在同一进程中用一些相同的活跃对象运行”,而非完全的纯数据 job 边界。

目标架构

1. 监听器阶段

DiscordMessageListener 仍然是入口点,但它的职责变为:

  • 运行预检和策略检查
  • 将接受的输入规范化为可序列化的 DiscordInboundJob
  • 将 job 入队到按会话或按频道的异步队列
  • 入队成功后立即返回给 Carbon

监听器不应该再拥有端到端的 LLM 回合生命周期。

2. 规范化 job 载荷

引入可序列化的 job 描述符,只包含稍后运行回合所需的数据。

最小形状:

  • 路由身份:agentIdsessionKeyaccountIdchannel
  • 投递身份:目标频道 id、回复目标消息 id、线程 id(如有)
  • 发送方身份:发送方 id、标签、用户名、tag
  • 频道上下文:guild id、频道名/slug、线程元数据、解析后的系统提示覆盖
  • 规范化消息体:基础文本、有效消息文本、附件描述符/已解析媒体引用
  • 门控决策:mention 要求结果、命令授权结果、绑定会话/agent 元数据(如适用)

job 载荷不能包含活跃的 Carbon 对象或可变闭包。

当前实现状态:

  • 部分完成
  • src/discord/monitor/inbound-job.ts 已存在并定义了 worker 交接
  • 载荷仍包含活跃的 Discord 运行时上下文,需进一步精简

3. Worker 阶段

添加 Discord 专属的 worker 运行器,负责:

  • DiscordInboundJob 重建回合上下文
  • 加载媒体和运行所需的额外频道元数据
  • dispatch agent 回合
  • 投递最终回复载荷
  • 更新状态和诊断

建议位置:

  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.ts

4. 排序模型

给定路由边界的排序必须与当前等效。

建议 key:使用与 resolveDiscordRunQueueKey(...) 相同的队列 key 逻辑。

这保持现有行为:

  • 一个绑定的 agent 会话不会与自身交错
  • 不同的 Discord 频道仍然可以独立推进

5. 超时模型

切换后有两个独立的超时类别:

  • 监听器超时:只覆盖规范化和入队,应该很短
  • 运行超时:可选的、worker 拥有的、显式的、用户可见的,不应意外从 Carbon 监听器设置继承

这移除了”Discord 网关监听器保持存活”和”agent 运行健康”之间的意外耦合。

建议实现阶段

阶段 1:规范化边界

  • 状态:部分实现
  • 已完成:提取了 buildDiscordInboundJob(...),添加了 worker 交接测试
  • 剩余:让 DiscordInboundJob 成为纯数据,将活跃运行时依赖移到 worker 拥有的服务,停止拼接活跃监听器 ref

阶段 2:内存 worker 队列

  • 状态:已实现
  • 已完成:按解析后运行队列 key 索引的 DiscordInboundWorkerQueue,监听器入队而非直接等待,worker 在进程内仅内存执行

阶段 3:进程拆分

  • 状态:未开始
  • 将投递、typing 和草稿流式所有权移到 worker 面向的适配器后面。

阶段 4:命令语义

  • 状态:未开始
  • 确保 stopnewreset 和未来会话控制命令在排队场景下正常工作。

阶段 5:可观测性和运维 UX

  • 状态:未开始
  • 发送队列深度和活跃 worker 计数到监控,记录时间戳和原因。

阶段 6:可选的持久性后续

  • 状态:未开始
  • 仅在内存版本稳定后决定是否需要重启持久性。

文件影响

当前主要文件:

  • src/discord/monitor/listeners.ts
  • src/discord/monitor/message-handler.ts
  • src/discord/monitor/message-handler.preflight.ts
  • src/discord/monitor/message-handler.process.ts
  • src/discord/monitor/status.ts

当前 worker 文件:

  • src/discord/monitor/inbound-job.ts
  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.test.ts
  • src/discord/monitor/message-handler.queue.test.ts

可能的下一批接触点:

  • src/auto-reply/dispatch.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/monitor/thread-bindings.ts
  • src/discord/monitor/native-command.ts

当前下一步

让 worker 边界从部分变为真正的:

  1. 将活跃运行时依赖从 DiscordInboundJob 移出
  2. 放到 Discord worker 实例上
  3. 将排队 job 精简为纯 Discord 专属数据
  4. 在 worker 内部从纯数据重建执行上下文

之后的后续是 stopnewreset 的命令状态清理。

测试计划

保持现有超时重现覆盖在 src/discord/monitor/message-handler.queue.test.ts

新增测试:

  1. 监听器在入队后返回,不等待完整回合
  2. 按路由排序保持不变
  3. 不同频道仍并发运行
  4. 回复投递到原始消息目标
  5. stop 取消 worker 拥有的活跃运行
  6. worker 失败产生可见诊断且不阻塞后续 job
  7. ACP 绑定的 Discord 频道在 worker 执行下仍正确路由

风险和缓解

  • 风险:命令语义从当前同步行为漂移。缓解:在同一切换中着陆命令状态管道。
  • 风险:回复投递丢失线程或 reply-to 上下文。缓解:让投递身份成为 DiscordInboundJob 中的一等公民。
  • 风险:重试或队列重启期间的重复发送。缓解:第一轮保持仅内存,或添加显式投递幂等性。
  • 风险:message-handler.process.ts 在迁移期间变得更难理解。缓解:在 worker 切换之前或期间拆分为规范化、执行和投递辅助。

验收标准

  1. Discord 监听器超时不再中止健康的长时间运行回合。
  2. 监听器生命周期和 agent 回合生命周期在代码中是独立概念。
  3. 现有的按会话排序保持不变。
  4. ACP 绑定的 Discord 频道通过同一 worker 路径工作。
  5. stop 瞄准 worker 拥有的运行而非旧的监听器拥有的调用栈。
  6. 超时和投递失败成为显式的 worker 结果,而非静默的监听器丢弃。

剩余着陆策略

在后续 PR 中完成:

  1. DiscordInboundJob 成为纯数据,将活跃运行时 ref 移到 worker 上
  2. 清理 stopnewreset 的命令状态所有权
  3. 添加 worker 可观测性和运维状态
  4. 决定是否需要持久性或显式记录内存边界

如果保持仅 Discord 且继续避免过早的跨频道 worker 抽象,这仍然是一个有界的后续工作。