最近在运行 OpenClaw 时,后台日志里不断出现类似内容:
[delivery-recovery] Delivery entry ... delivery state is send_attempt_started;
refusing blind replay without adapter reconciliation
[delivery-recovery] Outbound delivery retry: retry failed ...
而且同一批记录会隔几秒重复出现,日志量非常大。
一开始看起来像是 OpenClaw 自己的 recovery 机制出了问题,但继续排查后发现,真正的原因并不是日志系统,而是 消息投递失败后留下了未正确收尾的 delivery 状态。
日志中持续出现:
delivery state is send_attempt_started
refusing blind replay without adapter reconciliation
同时还有:
Outbound delivery retry: retry failed
更关键的是,同一个 cron delivery ID 会不断重复。
例如:
cron-direct-delivery:v1:cron:water-reminder-every-10min:...
以及其他 cron task ID。
这意味着 OpenClaw 的 delivery recovery 线程正在持续扫描这些历史记录,但每次都无法完成恢复。
最开始以为只是旧日志没有清理,但即使清空 Docker 日志,新日志仍然会马上重新出现。
这说明问题不是:
Docker 日志太多
而是:
OpenClaw 仍在持续产生这些日志
OpenClaw 的数据目录为:
/openclaw-data/.openclaw32
对应的 SQLite 主状态库:
/openclaw-data/.openclaw32/state/openclaw.sqlite
最开始使用系统自带的 SQLite:
sqlite3 --version
发现版本只有:
3.7.17
而 OpenClaw 数据库已经使用了较新的 SQLite STRICT table,因此会出现:
malformed database schema (...) - near "STRICT": syntax error
将 sqlite3 升级到 3.45.3 后,可以正常查看数据库:
sqlite3 /openclaw-data/.openclaw32/state/openclaw.sqlite ".tables"
其中有一个非常关键的表:
delivery_queue_entries
查看表结构:
sqlite3 /openclaw-data/.openclaw32/state/openclaw.sqlite \
"PRAGMA table_info(delivery_queue_entries);"
可以看到这些字段:
queue_name
id
status
entry_kind
session_key
channel
target
account_id
retry_count
last_attempt_at
last_error
recovery_state
platform_send_started_at
entry_json
enqueued_at
updated_at
failed_at
其中最关键的是:
status
recovery_state
last_error
retry_count
继续查询卡在 send_attempt_started 的记录:
sqlite3 -header -column \
/openclaw-data/.openclaw32/state/openclaw.sqlite "
SELECT
queue_name,
id,
status,
recovery_state,
retry_count,
channel,
target,
account_id,
last_error
FROM delivery_queue_entries
WHERE status = 'send_attempt_started'
OR recovery_state = 'send_attempt_started'
ORDER BY updated_at DESC
LIMIT 100;
"
最终发现真正的错误是:
errcode: 93006
errmsg: invalid chatid
也就是说,OpenClaw 调用企业微信发送消息时,传入的目标 ID 不正确。
数据库中的 target 包含:
HeYangYang
user:HeYangYang
channel:HeYangYang
这些值被发送到企业微信后,被接口判定为:
invalid chatid
所以整个链路实际上是:
Cron 定时任务触发
↓
OpenClaw 创建 delivery
↓
开始发送企业微信
↓
状态进入 send_attempt_started
↓
企业微信返回 93006 invalid chatid
↓
发送失败
↓
delivery 的 recovery_state 仍然停留在
send_attempt_started
↓
delivery-recovery 再次扫描
↓
无法确认消息是否已经真正发送
↓
拒绝 blind replay
↓
打印日志
↓
下一轮继续扫描
日志中的:
refusing blind replay without adapter reconciliation
其实是一种保护机制。
因为对于 OpenClaw 来说:
send_attempt_started
意味着:
我已经开始尝试发送消息,但我现在不能百分百确定外部平台到底有没有收到。
如果此时直接重发,就可能产生重复消息。
所以 OpenClaw 宁愿拒绝自动重放,也不会贸然再次发送。
问题在于,这些记录没有被正确转成最终状态,于是 recovery 会一直扫。
实际查询中可以看到,一部分记录已经是:
status = failed
recovery_state = send_attempt_started
另一部分 cron delivery 则是:
status = pending
recovery_state = send_attempt_started
并且 last_error 都是企业微信的 93006 invalid chatid。
因为这些状态不是保存在 Docker 临时文件里,而是持久化在 SQLite 中。
因此:
docker restart
或者:
docker compose down
docker compose up -d
都不会解决。
甚至重新创建容器以后,这些记录仍然存在。
OpenClaw 启动之后会重新读取:
delivery_queue_entries
然后 recovery 继续工作。
所以现象就是:
重启
↓
日志暂时消失
↓
OpenClaw 启动完成
↓
delivery-recovery 开始扫描
↓
继续刷屏
解决这个问题分两部分:
第一部分:清理已经卡死的历史 delivery。
第二部分:修复企业微信 target/chatid,避免继续产生新记录。
操作数据库之前,建议先停止 OpenClaw:
docker stop openclaw32-openclaw-gateway-1
然后备份:
cp /openclaw-data/.openclaw32/state/openclaw.sqlite \
/openclaw-data/.openclaw32/state/openclaw.sqlite.bak.$(date +%Y%m%d%H%M%S)
先统计:
sqlite3 -header -column \
/openclaw-data/.openclaw32/state/openclaw.sqlite "
SELECT
status,
recovery_state,
COUNT(*) AS cnt
FROM delivery_queue_entries
WHERE channel='wecom'
AND last_error LIKE '%invalid chatid%'
GROUP BY status,recovery_state;
"
确认这些都是历史失败消息后,可以清理:
sqlite3 /openclaw-data/.openclaw32/state/openclaw.sqlite "
DELETE FROM delivery_queue_entries
WHERE channel='wecom'
AND recovery_state='send_attempt_started'
AND last_error LIKE '%invalid chatid%';
"
然后确认:
sqlite3 /openclaw-data/.openclaw32/state/openclaw.sqlite "
SELECT COUNT(*)
FROM delivery_queue_entries
WHERE channel='wecom'
AND recovery_state='send_attempt_started'
AND last_error LIKE '%invalid chatid%';
"
如果返回:
0
说明已经清理完成。
然后启动:
docker start openclaw32-openclaw-gateway-1
观察:
docker logs -f --tail 100 openclaw32-openclaw-gateway-1
正常情况下,原来那批 delivery-recovery 日志应该消失。
仅删除数据库记录,只能解决历史问题。
如果代码继续把:
HeYangYang
user:HeYangYang
channel:HeYangYang
当成企业微信 chatid 发送,那么新的任务依然会:
发送
↓
93006 invalid chatid
↓
send_attempt_started
↓
delivery-recovery
↓
继续刷日志
因此真正的根治方案是检查 WeCom Adapter 的 target 路由。
需要明确区分:
userid
chatid
external_userid
群聊 chatid
单聊 userid
不能把 OpenClaw 内部的:
user:xxx
channel:xxx
直接当成企业微信 API 的 chatid。
这也是此次问题最核心的地方。
从实际失败数据看,所有这些记录的 last_error 都明确指向 93006 invalid chatid,说明企业微信目标解析就是当前主要故障点。
即使修复了业务问题,也建议给 Docker 开启日志轮转。
在 docker-compose.yml 中加入:
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
例如:
services:
openclaw-gateway:
image: openclaw-openclaw32:0821
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
这样最多保存约:
50MB × 3
避免异常情况下日志占满磁盘。
如果只是临时清空当前 Docker 日志,可以:
truncate -s 0 "$(docker inspect \
--format='{{.LogPath}}' \
openclaw32-openclaw-gateway-1)"
但要注意:
清日志并不能解决 delivery-recovery 本身。
只要数据库里的异常 delivery 还在,它马上还会继续写。
这次问题表面上是:
OpenClaw delivery-recovery 疯狂刷日志
实际上真正原因是:
WeCom target/chatid 错误
↓
企业微信返回 93006
↓
delivery 发送失败
↓
recovery_state 卡在 send_attempt_started
↓
OpenClaw 为避免重复发送拒绝 blind replay
↓
recovery 不断扫描同一批记录
↓
日志持续刷屏
所以完整解决方法应该是:
升级 SQLite 工具
↓
定位 delivery_queue_entries
↓
确认 last_error
↓
备份数据库
↓
清理历史 invalid chatid delivery
↓
修复 WeCom target/chatid 映射
↓
增加 Docker 日志轮转
这次排查也说明一点:
看到 recovery 日志一直重复时,不要第一时间关日志。
日志只是结果。
真正应该追的是:
delivery 为什么一直无法进入最终状态?
找到这一层,问题才算真正解决。