Discord 异步入站 Worker 计划
目标
将 Discord 监听器超时从用户可见的失败模式中移除,方法是让入站 Discord 回合异步化:
- Gateway 监听器快速接受和规范化入站事件。
- Discord 运行队列按现有排序边界存储序列化任务。
- Worker 在 Carbon 监听器生命周期之外执行实际的 agent 回合。
- 运行完成后将回复投递回原始频道或线程。
这是排队的 Discord 运行在 channels.discord.eventQueue.listenerTimeout 超时而 agent 运行本身仍在正常进行时的长期修复方案。
当前状态
本计划已部分实现。
已完成:
- Discord 监听器超时和 Discord 运行超时现在是独立设置。
- 被接受的入站 Discord 回合入队到
src/discord/monitor/inbound-worker.ts。 - Worker 现在拥有长时间运行的回合,而非 Carbon 监听器。
- 现有的按路由排序保持不变。
- 存在 Discord worker 路径的超时回归覆盖。
通俗地说:
- 生产超时 bug 已修复
- 长时间运行的回合不再因为 Discord 监听器预算到期就死掉
- worker 架构还没完成
仍然缺少的:
DiscordInboundJob仍然只是部分规范化,仍携带活跃运行时引用- 命令语义(
stop、new、reset、未来会话控制)还未完全 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 描述符,只包含稍后运行回合所需的数据。
最小形状:
- 路由身份:
agentId、sessionKey、accountId、channel - 投递身份:目标频道 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.tssrc/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:命令语义
- 状态:未开始
- 确保
stop、new、reset和未来会话控制命令在排队场景下正常工作。
阶段 5:可观测性和运维 UX
- 状态:未开始
- 发送队列深度和活跃 worker 计数到监控,记录时间戳和原因。
阶段 6:可选的持久性后续
- 状态:未开始
- 仅在内存版本稳定后决定是否需要重启持久性。
文件影响
当前主要文件:
src/discord/monitor/listeners.tssrc/discord/monitor/message-handler.tssrc/discord/monitor/message-handler.preflight.tssrc/discord/monitor/message-handler.process.tssrc/discord/monitor/status.ts
当前 worker 文件:
src/discord/monitor/inbound-job.tssrc/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.test.tssrc/discord/monitor/message-handler.queue.test.ts
可能的下一批接触点:
src/auto-reply/dispatch.tssrc/discord/monitor/reply-delivery.tssrc/discord/monitor/thread-bindings.tssrc/discord/monitor/native-command.ts
当前下一步
让 worker 边界从部分变为真正的:
- 将活跃运行时依赖从
DiscordInboundJob移出 - 放到 Discord worker 实例上
- 将排队 job 精简为纯 Discord 专属数据
- 在 worker 内部从纯数据重建执行上下文
之后的后续是 stop、new 和 reset 的命令状态清理。
测试计划
保持现有超时重现覆盖在 src/discord/monitor/message-handler.queue.test.ts。
新增测试:
- 监听器在入队后返回,不等待完整回合
- 按路由排序保持不变
- 不同频道仍并发运行
- 回复投递到原始消息目标
stop取消 worker 拥有的活跃运行- worker 失败产生可见诊断且不阻塞后续 job
- ACP 绑定的 Discord 频道在 worker 执行下仍正确路由
风险和缓解
- 风险:命令语义从当前同步行为漂移。缓解:在同一切换中着陆命令状态管道。
- 风险:回复投递丢失线程或 reply-to 上下文。缓解:让投递身份成为
DiscordInboundJob中的一等公民。 - 风险:重试或队列重启期间的重复发送。缓解:第一轮保持仅内存,或添加显式投递幂等性。
- 风险:
message-handler.process.ts在迁移期间变得更难理解。缓解:在 worker 切换之前或期间拆分为规范化、执行和投递辅助。
验收标准
- Discord 监听器超时不再中止健康的长时间运行回合。
- 监听器生命周期和 agent 回合生命周期在代码中是独立概念。
- 现有的按会话排序保持不变。
- ACP 绑定的 Discord 频道通过同一 worker 路径工作。
stop瞄准 worker 拥有的运行而非旧的监听器拥有的调用栈。- 超时和投递失败成为显式的 worker 结果,而非静默的监听器丢弃。
剩余着陆策略
在后续 PR 中完成:
- 让
DiscordInboundJob成为纯数据,将活跃运行时 ref 移到 worker 上 - 清理
stop、new和reset的命令状态所有权 - 添加 worker 可观测性和运维状态
- 决定是否需要持久性或显式记录内存边界
如果保持仅 Discord 且继续避免过早的跨频道 worker 抽象,这仍然是一个有界的后续工作。