Blocked-by: objectstack-ai/objectstack#5160(同文件 `email-plugin.ts`,严格串行;且 #5160 落地后本单的正确修法才定形 —— 清扫出的行在队列模式下应交给队列,而不是自己直接投) PM 在回答维护者「邮件有没有队列」时读码发现,维护者已确认要修。 ## 现状 `sys_email` 的 `status: 'queued'` 行只有一个消费时机:**insert 当时**的 `afterInsert` drain 钩子(`email-plugin.ts` ~390,`setTimeout(0)` 异步自投)。全仓 grep 过:没有任何定时任务、启动清扫或 worker 会再看一眼 `queued` 行。 两个后果: 1. **崩溃窗口**:进程在 insert 之后、投递完成之前死掉,该行永远停在 `queued`。一个叫 queued 的状态没有队列在消费它 —— declared ≠ delivered 的又一实例,只是这次声明的是状态词。 2. **失败静默**:drain 钩子的两层 catch 都只 `logger.warn` 就吞掉(`outbox drain failed for …` / `outbox drain hook error`)。按 AGENTS.md degradation-log-level 标准,这是「投递没有发生」级别的失败,应为 error 且带后果与修复;warn 会被启动日志淹没。 ## 建议 1. `kernel:ready` 时做一次启动清扫:捞 `status='queued'` 且非本进程 managed 的行 → 队列模式下 `publish` 给 `email.send.async`(#5160 的通道),内联模式下 `deliverPersistedRow`;清扫结果(捞了几行、成败各几)打 info 一行。 2. 是否加周期性清扫由实现判断 —— 若 #5160 的队列模式已覆盖新写入的行,周期清扫可能只对内联模式有意义,不要为覆盖率造第二套调度。 3. drain 钩子的失败升 error,消息含后果(这封信没有发出,行停在 queued)与修复(重试入口/清扫会捞)。 ## 验收 - 人为构造「insert 后进程死亡」的行(直接插 queued 行不触发钩子即可模拟),重启后该行被推进到 sent/failed,不再永久滞留; - drain 失败的日志级别与文案有用例钉住; - 默认路径(正常 insert→投递)行为不变。
Activity
os-zhuang commentedon Aug 4, 2026
认领:PM 循环第 6 轮
会话:
session_017MCKJaEomEqg4tvz4SzdNd分支:
claude/issue-5161-sys-email-queued-sweepWorktree:
objectstack-issue-5161域:
domain:services文件面:
packages/plugins/plugin-email/src/email-plugin.ts、packages/plugins/plugin-email/src/email-service.ts(如需)、同包测试(越界即停,报告说明)Blocked-by 已解除:#5160 的 PR #5173 已合入(
9c4f1743c),落地判据核过完好。与 #5179 并批(service-queue 包,文件面不相交)。注意 #5177 排在本单之后、会碰同包的 email-service.ts —— 本单不要顺手动 headers/attachments 相关的任何东西。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
复核结论:ACCEPT —— PR #5191,转 ready 并入合并队列。最终 head
13464b40(门禁修复)上 23 个 check run 全部 success/skipped。实现复核(对 diff 独立核过)
managedRowIds与sent/message_id两道兜底,与订阅者幂等守卫同套语义。5 分钟的选值有依据(内联重试退避上限 2s,活着的投递早已定稿)。enqueuePersistedRow,内部就是send()自己的publishRow—— 同一常量、同一 publish options、同一幂等键。还挂着 pending 作业的行被清扫时收敛到那个作业,而不是推第二个 worker 上去;端到端用例证实「已有 pending 作业时清扫不再产生第二个作业」。kernel:ready传播语义(LiteKernel 吞异常,core: kernel:ready 钩子抛错在 ObjectKernel 上失败 boot、在 LiteKernel 上被吞成一条 error 日志 —— 同一钩子两套失败语义 #5170 在跟);内联模式无跨进程协调直说了——那是 drain 钩子从来就有的边界。deliverPersistedRow进了DURABILITY_CRITICAL_CALLEES,以后有人把它的失败降级会被门禁挡住。门禁往返一次(今日第三例,
assertEngineDeleteDispatch,已记 #5197),修复与b169f217同形。至此 #5160 的两个直接后续全部落地在途:本单(sys_email 侧)与 #5179(sys_job_queue 侧,PR #5192 已入队)。
Generated by Claude Code
fix(plugin-email): sys_email 的 queued 行在启动时被清扫,drain 失败升为 error (obje…
feat(plugin-email,platform-objects): sys_email 加 headers_json + 有界 at…
fix(plugin-email): send() releases an insert-assigned row id from man…