feat(amsg): tick 支持同分组任务串行,新增 fire 收尾 hook,任务详情可读回完整 metadata - #44
Merged
Conversation
- `runScheduledTick` 新增可选 `serializeBy(task)`:返回同一个键的任务,同一跳只放行 一条,且该组还有任务正在跑时后来的也不放行。被拦下的任务不动任何字段(next_send_at / status / retry_count 全部原样),下一跳照常再被捞起来。跨跳的判定与占位在 `claimTask` 的同一条 UPDATE 里完成,分组键以 HMAC 派生值落库,不为分组多开明文出口。 - `lease_until` 与 `retry_after` 拆开:前者只表示「正在跑」,投递失败的退避改写后者 并立刻放掉租约。此前两件事共用一列,一条正在退避、其实闲着的任务会把整组堵住。 - 新增 `onFireSettled(ctx, outcome)`:只要 `onBeforeFire` 被调用过就必调一次, `status` 区分 sent / skipped / failed / not-handled,部分失败带 sentCount 与 total。 宿主用它做「拿了就要放」的收尾(记账、放锁)。`onAfterSend` 语义不变。 - 新增 `GET /message?id=<uuid>`:返回单条任务详情并带完整 metadata(凭据仍剔除), 列表投影保持不带。客户端对应 `client.getMessage(uuid)`。 - `PUT /update-message` 的可写字段增加 `contactName`,要求非空字符串。 - amsg-sw:通知正文为空时退回 `defaults.defaultBody`,仍照常弹出——不用「不弹」解决, 那会违反 userVisibleOnly 约定。 新增三列 `lease_until` / `retry_after` / `serialize_group`,`initSchema` 幂等补齐。 Claude-Session: https://claude.ai/code/session_01DpV5FADagj4MTUMo4r9UUX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
起因是下游 SullyOS 排查主动消息「穿帮」时发现的几处,判断下来在库这边解决比在宿主绕更根本。
改了什么
同分组任务不并发跑 —
runScheduledTick新增可选serializeBy(task)。同一个角色挂多条定时任务时,两条撞在一起会让用户一口气收到两条互不知情的消息;更麻烦的是宿主在 hook 里维护的台账是读-改-写整份,并发时后写的会盖掉先写的。危险窗口不只是「同一跳」:一次 fire 含 LLM 生成加工具循环,几十秒到几分钟很常见,上一跳还在跑时下一跳照样捞到同角色的另一条。所以同跳与跨跳都拦,跨跳的判定与占位放在
claimTask的同一条 UPDATE 里(先查后占中间的空档能让两个 tick 双双进同一组)。被拦下的任务一个字段都不动,下一跳原样再被捞起来 — 是推迟,不是丢弃。
lease_until与retry_after拆开 — 此前一列兼职「正在跑」和「等着重试」两件事,一条正在退避、其实闲着的任务会把整组堵住最长 6 分钟。现在退避写retry_after并立刻放掉租约。onFireSettled—onAfterSend只在真发出去那条路触发,skip / skip-push / 抛错都不调,宿主拿不到可靠的「这次结束了」信号。下游因此踩过两次:hook 里建出来的任务漏了记账变成谁也删不掉的幽灵任务;想在 hook 里拿锁则找不到释放点。新 hook 只要onBeforeFire被调用过就必调一次,status区分 sent / skipped / failed / not-handled。onAfterSend语义不变。GET /message?id=<uuid>— 列表投影有意不带完整 metadata,但update-message对 metadata 是整体替换,两者相加导致「只改 metadata 里的一个子字段」在客户端做不到(拿不回完整的就没法读-改-写)。新端点返回单条详情并带完整 metadata,凭据仍按白名单剔除;列表投影保持不变。update-message认contactName— 白名单里有avatarUrl没有它,看着是漏了。它不是 key(投影注释里就写着会跨角色重名、宿主靠 charId 过滤),数据库层面也没有独立列。amsg-sw 空正文兜底 — 宿主发了 body 为空的 content push 时,此前会弹一条只有标题、正文空白的横幅。现在退回
defaults.defaultBody,仍然照常弹 — 没有用「不弹」来解决,那会违反userVisibleOnly约定(README 里那一段讲的代价)。Schema
新增三列
lease_until/retry_after/serialize_group,initSchema幂等补齐(SQLite 没有ADD COLUMN IF NOT EXISTS,只吞duplicate column name,其他错误照抛)。列不在时getPendingTasks会直接报错,整跳发不出去 — 部署时别出现「代码已经是新的、表还没迁」的窗口。测试
npm run ci全绿,server +28 / sw +4。回归守卫做了反向验证:临时改回旧行为(退避写回lease_until、claimTask不传分组参数、去掉onFireSettled),确认对应用例会红。同跳内的预处理改成按
next_send_at升序顺序执行 — 原本并发解密,谁先解完谁抢到组是随机的,同角色晚到点的消息可能抢在早到点的前面发出去。这也是此前测试偶发抖动的根因,连跑 6 轮已无抖动。已知边界
claimSkippedTasks计数里,「被分组拦下」与「行被别的 tick 领走」混在一起,要分开得让适配器回报失败原因,不值得。claimTask但忽略新增的分组参数,串行会静默退化成只在同一跳内生效,库无法探测;interface 与 README 已写明。onStaleSkip里宿主也在做读-改-写,并发同样会互盖。NOT EXISTS子查询。https://claude.ai/code/session_01DpV5FADagj4MTUMo4r9UUX