OpenCode 插件:智能检测静默中断并自动重试。
当 OpenCode 会话因网络抖动、中转网关超时(60s nginx 超时)或静默中断失败时,插件能识别不同类型的错误类型并重试,让工作持续推进不被打断。
- 连接断开:
ECONNRESET、Server disconnected、网络抖动 - 静默中断:0 输出、
finish=unknown,OpenCode 自己都检测不到的本质错误 - 超时断开:
Connection failed: Server disconnected without sending a response.
- 识别中转网关 60s 超时的伪造
finish=length+ 0 output + 空 parts - 避免下游组件(如大纲页时间轴)误判为"正常截断"
- 只对真正空白(无文本无工具调用)触发续写
- 子会话身份门禁:会话存在
parentID时,插件不会发送任何“继续”消息;不依赖 agent 名称或消息内容猜测 - 根编排等待保护:根会话调用工具等待子代理返回时,保留消息模式保护,避免自然 idle 被误判为中断
- idle、轮询、
session.error和延迟重试均走同一门禁;真正发送前会再次读取会话身份,避免定时器竞态误伤 - 无法读取会话身份时采用 fail-closed:宁可少重试一次,也不向未知会话注入消息
- 回合预算:每个用户回合最多重试 2 次;预算只在真正的用户新消息时重置,
session.idle清理不会抹掉计数 - 硬上限终态:达到上限后写入
exhausted终态并停止本回合所有自动续写,后续session.error无法重建预算(修复了「继续」曾无限循环的 bug) - 退避:5s → 10s(避免和网关 HTTP keepalive 冲突)
- 轮询兜底:60 秒无事件才轮询(极端情况)
session.error触发时直接用事件 error 对象,不依赖 DB 时序session.idle只结束本轮运行态,不删除回合预算(之前会整条删除,把重试计数清零)chat.message识别出自己注入的「继续」时不重置预算(之前注入标记会被 idle 清掉,误判成真用户消息)
回合预算与临时运行态分开存放(src/session-state.ts),这是修复无限续写的关键:
- 回合预算(
retryCount/blankCount/exhausted):只在真正的用户新消息时重置,idle 清理不得触碰 - 临时运行态(
roundActive/lastActivity/ 注入标记):可随回合结束复位,但同样不做整条删除
- 核心编排逻辑统一在
src/index.ts,会话状态分层在src/session-state.ts npm run build && npm run install-plugin部署- 与本地
autoretry.js分离,永远保持同步
git clone https://github.com/rayn1314/opencode-autoretry.git
cd opencode-autoretry
npm install
npm run build
npm run install-plugin安装脚本会将 dist/index.js 拷到 ~/.config/opencode/plugins/autoretry.js。
(待发布)
插件默认启用。如需临时关闭,在 opencode.json 中用元组格式注册插件并传入 enabled: false:
{
"plugin": [
["~/.config/opencode/plugins/autoretry.js", { "autoretry": { "enabled": false } }]
]
}关闭后插件不注册任何事件处理器,不会检测中断、不会发送"继续"、不会安排重试。
如果你用普通字符串格式注册插件(
"plugin": ["~/.config/opencode/plugins/autoretry.js"]), 也可以在opencode.json顶级放"autoretry": { "enabled": false },插件会在加载时通过client.config.get()读取。
插件还支持通过 /autoretry 命令在运行时启用/禁用,无需修改配置文件或重启 OpenCode:
/autoretry off → 关闭插件,清理所有 pending 定时器
/autoretry on → 重新开启插件
/autoretry status → 查看当前开关状态
插件在初始化时自动注册 /autoretry 命令到全局目录 ~/.config/opencode/command/autoretry.md,无需手动创建命令定义文件,且不会污染任何项目仓库。
如果该文件已存在则不会覆盖,避免覆盖你的定制。
旧版本曾把文件写到各项目的 .opencode/command/ 下,新版本会在加载时自动清理这些遗留文件(仅删除内容为 {{arguments}} 的自动生成文件,用户定制的同名文件保留)。
关闭后插件会立即清理所有 pending 的重制定时器和会话状态,不再检测中断或发送续写。 重新开启后恢复全部功能。
检测策略和退避参数目前使用硬编码默认值(单一真相源):
{
"autoretry": {
"enabled": true,
"maxRetries": 2,
"backoffMs": [5000, 10000],
"pollIntervalMs": 60000,
"retryOn": ["输出截断", "静默中断", "数据流截断", "连接断开", "请求超时", "请求参数错"]
}
}暂不可配的原因:
- 检测逻辑依赖 FFT(可视化反馈)
- 子代理等待防护当前对所有 agent 一致
- 空白检测已收敛为唯一出口,无需额外配置
| 分类 | 触发条件 | 奔溃行为 |
|---|---|---|
| 用户中止 | MessageAbortedError |
跳过,不打扰 |
| 输出截断 | output>0 | 重试发送"继续" |
| 静默中断 | 0 output + 无 error + finish=unknown | 重试发送上一条消息 |
| 数据流截断 | JSON parsing / Unterminated | 重试发送"继续" |
| 连接断开 | Connection failed / Server disconnected / ECONNRESET |
重试发送上一条消息 |
| 请求超时 | timeout / timed out |
重试发送上一条消息 |
| 请求参数错 | HTTP 400 / bad request |
重试发送上一条消息 |
| 限流 | HTTP 429 / rate limit |
toast 提醒,不重试 |
| 服务商异常 | HTTP 5xx / provider |
toast 提醒,不重试 |
session.error 事件 ← OpenCode 服务端
↓
直接提取 error.name + error.message(事件自带)
↓
分类错误类型
↓
可重试? → 否 → toast 提醒
↓
是 → 检查 retryCount < maxRetries
↓
是 → scheduleRetry(5s/10s)
特点:错误消息还没入库也能分类,不依赖 DB 时序。
用户发消息 → session idle
↓
已有 pending retryTimer → 只结束本轮运行态,保留定时器
↓
否则 → checkAndRetry 查消息列表
↓
预算已 exhausted → 跳过(本回合不再自动续写)
↓
finish=stop/length + output=0 → 空白完成检测
↓
finish=unknown → 分类错误,scheduleRetry
↓
结束本轮运行态 + 清 pollTimer(绝不动回合预算)
特点:session.idle 只复位临时运行态,不删除回合预算,因此重试计数能跨 idle 累积到上限。
60s 内无任何事件 → poll check 触发
↓
检查 lastActivity 是否 ≥ 60s
↓
是 → checkAndRetry(同 session.idle 路径)
↓
清空 pollTimer
用户发消息 → consumeOwnInjection(sessionID)?
├─ 是(autoretry 发的,标记有效)→ 只更新时间戳,不重置预算
└─ 否(真正用户 或 标记已过期)→ resetTurn:retryCount=0, blankCount=0, exhausted=false
特点:autoSendPending 标记现在存放在不会被 idle 清理的临时运行态里,
并带 60s 兜底有效期,既能识别自己注入的续写,也不会因标记卡死而误吞真正的用户消息。
- opencode-autoretry(本插件):运行在 OpenCode 内部,负责自愈
- opencode-watchdog(外部工具):独立进程,负责外部监控备份
两者互补,不冲突:
- 插件在内部自动重试,推进工作
- watchdog 在外部兜底,当插件重试失败或禁用时仍能提醒
插件使用 OpenCode 内置日志系统,可在 OpenCode 日志中查看:
service: autoretry
level: info
message: autoretry plugin loaded (idle-check)
message: detected interrupt: 连接断开
message: retry sent (1)
message: subagent wait detected, skipping all retry/continuation
session.error:最后一条 assistant 消息有 errorsession.idle:最后一条 assistant 消息无 error + finish=undefinedpoll check:最后一条 assistant 消息无 error + finish=undefined + 超过 60s
session.idle检测到finish=stop/finish=length→ 不走分类- 子代理等待(前条消息有 tool calls + 当前无 error + output=0) → 不发续写
- finish=stop 且有文本/工具 → 不触发空白完成续写
# 安装依赖
npm install
# 运行回归测试(会先编译 TypeScript)
npm test
# 编译 TypeScript
npm run build
# 类型检查
npm run typecheck
# 部署到全局插件目录
npm run install-plugin仓库提供 4 个只读诊断脚本,直接查询 OpenCode 的 SQLite 数据库,帮助定位异常消息和验证 autoretry 行为。
扫描数据库中的异常消息模式,快速掌握整体状况。
npm run diagnose # 扫描最近 7 天
npm run diagnose -- 30 # 扫描最近 30 天
npm run diagnose -- 0 # 扫描全部记录检测项:
- 空白完成(finish=stop/length + output=0)
- 中转网关伪造 length(finish=length + output=0 + input>0)
- 连接断开(Connection failed / disconnected / ECONNRESET)
- 静默中断(finish=undefined + output=0 + 无 error)
- 零 token 消息(input=0 + output=0)
- autoretry 触发记录("继续"消息)
查看特定 session 的完整消息流,包括角色、finish、token、错误、工具调用。
npm run timeline -- <session-id>
# 示例:
npm run timeline -- ses_0c82b5cf5ffeDHr2ayODq7mlX8输出包含每条消息的 finish 状态、token 用量、error 详情、工具调用摘要。如果发现错误消息,会自动检查错误后是否有 autoretry 介入。
按关键词搜索消息,查看完整 JSON 结构和上下文。
npm run errors -- "Connection failed"
npm run errors -- "disconnected"
npm run errors -- "UnknownError"
npm run errors # 不带参数:列出最近 10 条有 error 的消息每条匹配消息会显示:role、finish、tokens、error 完整结构、provider/model,以及前一条 assistant 消息的状态(用于判断 isSubagentWait 是否应该拦截)。
查找 autoretry 发送的"继续"消息,显示每次触发的上下文和结果。
npm run trace # 最近所有触发
npm run trace -- 7 # 最近 7 天
npm run trace -- <session-id> # 特定 session每次触发显示:
- 触发原因(前一条 assistant 的 finish、output、error、hasTool)
- 触发类型(空白完成续写 / 中断重试 / 输出截断续写)
- 重试结果(✓ 成功 / ✗ 失败 / ? 仍空白)
脚本自动定位 ~/.local/share/opencode/opencode.db。可通过环境变量覆盖:
OPENCODE_DB=/path/to/opencode.db npm run diagnose- 依赖 OpenCode 事件结构:
session.error的 payload 结构未在文档中明确,当前做防御性处理 - 配置读取:当前配置硬编码在
src/index.ts,未来可扩展为全局配置 - 重试内容:当前只重发用户消息的
parts,不处理 attachments
MIT