pi-resilient-retry0.1.2
Keep retrying failed provider requests that Pi's built-in retry gives up on
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-resilient-retry
先让 Pi 执行原生恢复;仍失败时,插件才继续同一任务。
这是与模型协议无关的 AgentSession 兜底流程,不是新增「带重试的模型入口」。
在 Pi、Percho 中继续选择现有模型即可;协议、鉴权和流处理仍由原 SDK 承担。
使用 @earendil-works/pi-coding-agent 1.4 系列的公开扩展 API。
English | 中文
推荐搭配安装
本扩展解决「任务不被打断」。如果你还想在自动重试之外手动精准续跑, 请同时安装 pi-retry:
| pi-resilient-retry(本扩展) | pi-retry | |
|---|---|---|
| 触发方式 | 自动,失败即重试 | 手动敲 /retry |
| 定位 | 后台重试,任务不中断 | 精准续跑,不重放已完成操作 |
| 重试次数 | 插件续跑次数默认不限,有重复错误保护 | 以该扩展的手动重试策略为准 |
| 历史修复 | 明确 ID 失效时剥 id;资源错配再发作时深度恢复 | /retry 剥 id,--deep 连 reasoning 一起剥 |
两者触发方式不同。使用手动 /retry 前先停止自动恢复;本轮没有重新验证搭配安装的行为。
解决什么问题
Pi 本身有自动重试:过载、429、5xx、超时、流中断,最多重试 retry.maxRetries 次。
除此之外的错误会立刻结束当前运行。这正是中转站把长任务搞死的原因:上游返回 400、
未映射的文案如 upstream sent an error event、或中转站特有的拒绝。
本扩展在 agent_before_settle(Pi 中最后一个能强制再发一次模型请求的边界)加了一层兜底。
当运行因可重试错误结束、且用户没有中止时,先启动原生续跑,再在请求发出前等待退避。
403: {"code":"SUBSCRIPTION_NOT_FOUND","message":"No active subscription found for this group"}
也按上游故障默认重试,不需要开启 retryQuotaErrors。沿用原有退避、重试预算及重复错误保护,
不会因这条错误触发历史修复。
从 npm 安装
npm 包:pi-resilient-retry。
请用 Pi 的包管理器安装,才能按包内声明加载扩展入口:
# 安装 npm 最新已发布版本
pi install npm:pi-resilient-retry
# 固定到指定版本
pi install npm:pi-resilient-retry@0.1.2
需要 Node.js 22.19+ 及上文兼容的 Pi 扩展 API。重启 Pi,或在空闲时 /reload。
无需新模型 / provider / 鉴权配置,前提是宿主向该会话加载本扩展。
npm 安装不会消除下文的 Percho 内置子会话限制。
更新 npm 安装
# 更新这个已安装包(npm 来源没有固定版本时)
pi update npm:pi-resilient-retry
# 或更新全部已安装包
pi update --extensions
之后重启或在空闲时 /reload。固定到 @0.1.0 的来源仍停留在该版本,
要升级固定版本请显式安装新版本号。Git commit / push 不等于 npm 发布,
升级 Pi CLI 也不等于自动升级这个包。
Git、本地开发与迁移
pi install git:github.com/ZhiAo1120/pi-resilient-retry
pi install /path/to/pi-resilient-retry
本扩展请只保留一个安装来源。从 Git / 本地迁移到 npm 时,先 pi list
确认旧来源,移除后再装 npm;不要同时加载两份:
pi remove git:github.com/ZhiAo1120/pi-resilient-retry
pi install npm:pi-resilient-retry
原来从本地安装的,请移除 pi list 显示的真实路径,而不是上面的 Git 来源。
npm 发布前试用本地改动
修改这个仓库不会自动更新已安装的 npm 副本。要试用本地包,先移除 npm 来源,
再安装此目录,之后重启宿主或在空闲时 /reload:
pi remove npm:pi-resilient-retry
pi install /path/to/pi-resilient-retry
使用与宿主相同的 agent 目录/配置作用域,且只保留一个来源。 这不会发布 npm 新版本,也不会更新其他安装。
配置
全部可选。
{
"resilientRetry": {
"enabled": true,
"maxRetries": 0,
"baseDelayMs": 2000,
"maxDelayMs": 60000,
"repeatLimit": 30,
"retryQuotaErrors": false,
"repairHistory": true
}
}
| 配置项 | 默认值 | 含义 |
|---|---|---|
enabled |
true |
关闭扩展而不卸载。 |
maxRetries |
0 |
无成功请求时允许的连续插件续跑次数。0 = 不限,不是总 HTTP 请求上限。 |
baseDelayMs |
2000 |
首次退避延迟;每次翻倍。 |
maxDelayMs |
60000 |
延迟上限,所有延迟都限制在此值内。 |
repeatLimit |
30 |
连续相同的终态错误;忽略 trace_id / request_id 的值并规范化空白后比较完整文本。0 关闭该保护。 |
retryQuotaErrors |
false |
也重试额度/计费错误。 |
repairHistory |
true |
明确 ID 失效时允许剥 id;资源/账号错配时仅在冻结的旧历史边界内尝试一次深度恢复。 |
debugLog |
未设 | 可选 JSONL 决策元数据文件;不记录原始错误、提示、载荷、鉴权或 headers。 |
预算在成功、被中止的结算、会话初始化后重置,限制连续终态故障,而非整个会话。
数值接受非负有限值;无效数值使用默认值。小数延迟保留(实际精度受定时器限制)。
旧的 visibleRetry、noticeEvery 配置不再生效,可删除;即使保留为 true 也不会新增消息标记。
重试是怎么发生的
重试是系统重新调用模型,不是你发言。扩展在边界返回一个续跑指令,把失败的助手回合从上下文 丢弃后重发请求。全程不会代替你发送任何消息:
会话:user "hi" → assistant(报错)→ [context omit] → assistant(报错)→ [context omit] → assistant "回答"
请求:始终是原始消息和历史;普通重试的载荷保持相同
失败的尝试用 context_edit 从后续模型上下文中排除,因此普通重试不增加提示、不丢历史记忆。
原始错误记录和 context_edit 仍保留在只追加的会话文件中,并非删除原始记录或让文件完全不增长。
前序扩展提出的边界编辑保持不变,插件只追加自己针对失败请求的编辑。
不再通过消息显示重试
不注入 user、assistant 或 custom_message,不要求模型输出「正在重试」。旧版本生成的
customType: "resilient-retry" 消息只在构造模型输入时按身份排除,不改写会话文件。
正常用户消息、助手回答、工具结果即使提到 pi-resilient-retry 也完整保留。
早期版本写成真实用户发言的提示不会按文本猜测并自动删除。
为什么重试不能只是「再发一遍同样的请求」
中转站会把每次尝试派给不同的上游节点。因此原样重放同一份载荷可能永远失败:
节点 A 生成的历史项带着 id,节点 B 拒绝解析
(The requested item was created under a different Azure OpenAI resource)。
只有当上游明确报告本会话的 id 不可用时才修复:
| 触发文案 | 说明 |
|---|---|
created under a different Azure OpenAI resource |
中转站池化场景中实际观测到的失败 |
created under a different *** OpenAI resource |
含字面 *** 的同类资源错配,执行相同的 ID 修复 |
belongs to a different upstream account |
同类重放拒绝 |
previous_response_id / item / session … not found | expired | invalid |
id 已无法解析 |
恢复分级进行,仍遵守原有重试预算和重复错误保护:
- 普通 ID 修复:仅移除完整、可重放项的可选
id。正文、图片、工具调用及结果、 配对用的call_id和 reasoning 均保留。一般的缺失 ID 错误仍只修复一次续跑请求。 - 冻结资源错配边界:针对资源/账号错配,在修复前记录故障请求完整
input的指纹前缀。 同一选中 provider/API/model 后续仅抑制这个旧前缀里的可选 ID,不把新增状态纳入范围。 指纹只在内存中保留,不存储一份新的对话正文。 - 一次有界深度恢复:普通 ID 修复后仍遇到资源/账号错配时,仅舍弃该旧前缀里 完整的 reasoning 项及其加密内容。可见历史及工具配对保留,旧隐藏推理上下文会损失。
- 恢复后保留新状态:后续请求继续抑制同一旧前缀,但保留新增 reasoning、加密内容、 消息和工具项的 ID,包括新追加但内容恰好与旧项相同的情况。成功或 Stop 不扩大此边界。
- 再次错配就止损:一次深度恢复发出后仍发生资源/账号错配,就停止插件自动续跑; 不启动第二轮有损恢复。普通临时错误仍遵循原有重试策略。
如果原前缀已被压缩、改写或缩短,就不再猜测哪些相同内容属于旧历史,不做该边界的清理。 其他选中的模型不受影响。重载或切换会话会清除内存边界;重新加载后再报错可能建立新边界, 不要用反复重载来绕过连续性保护。
普通错误及一般的 item not found 不触发深度恢复。没有资源恢复边界的模型,其普通请求仍不变;
SDK 路由和其他扩展仍拥有各自的转换行为。repairHistory: false 同时关闭两级修复。
不会改 wire model ID、消息的 provider/API、call ID、存储消息里的签名或原始会话记录。
无法取得可验证的完整请求快照,或遇到 previous_response_id、conversation、item_reference,
会拒绝资源错配自动恢复:仅删除服务器引用可能悄悄丢失历史。也不清除载荷钩子之后由 transport
注入的状态,包括缓存 WebSocket 续接。未知或不完整项仍保留,因此不是所有资源错误都能修复。
资源错配假上游实际检查旧状态,并验证恢复后新 reasoning 的重放;这仍不等于真实供应商验收。
连续性与可信度
此策略限制的是损失范围,不能重建已舍弃的旧隐藏推理,也不能保证恢复后结论与原分析等价。 成功响应只证明请求可以继续;长任务的关键约束、结论和下一步仍需结合保留的原始记录、 工具证据与测试重新核对。扩展不伪造记忆、不注入“已经重新验证”的提示。
如何看到重试正在进行
复用 Pi / Percho 原生的智能体执行中效果,不依赖 Percho 不消费的扩展 UI 通道:
失败 → agent_end → agent_before_settle(立即决定续跑)
→ agent_start → turn_start → context(等待退避)→ 请求上游
- 原生活动:真实
agent_start、turn_start在退避前发出,宿主可以使用原有执行状态。 插件不伪造事件、错误卡或结果 pane。 - 终端 Pi:宿主实现
setWorkingMessage时,工作文字从「等待重试(第 N 次)」切为「正在重试(第 N 次)」。 - 取消 / 结束:用宿主 Stop 或关闭会话。AbortSignal 监听器立即唤醒退避;定时器和监听器均清理,
大延迟也不会溢出。成功、中止、达到限制后清除本扩展的工作文字。
直接 dispose 时,通过真实 abort 信号在 SDK 使上下文失效前清除 UI;终态回调不访问已关闭上下文。
每个低层
agent_end都交还插件工作文字,再由使用独立控制器的 SDK 原生退避接管。 后期结算只使用事件数据和缓存的 run 元数据,不读可能失效的 context getter。 - 验收边界:已验证 SDK / RPC 活动和取消;没有观察交互式 TUI、完整 Percho 桌面动效或 pane 视觉。
退避不能放在 agent_before_settle 内:此时低层 agent_end 已发出,宿主已经关闭执行动效。
也不能阻塞原生开始事件的处理器:Pi 要等扩展处理器返回后才把该事件通知 UI。
因此等待放在两个开始事件之后的 context 钩子里;等待本身不修改任何消息。
每次低层运行结束与续跑开始之间仍有正常的生命周期切换,可能短暂复位或留下宿主原生错误卡。 扩展不伪造生命周期事件,也不修改 Percho 来隐藏这些记录。精确次数和失败原因可查下述调试日志。
阻止无解的循环
重复错误保护用于阻止同一错误的无效循环,不证明变化中的错误就安全或低成本。
需要限制插件预算时,请设置有限 maxRetries。指纹忽略标记为 trace_id / request_id 的值,
规范化空白后比较完整错误;其余错误细节仍有区别,不会因长错误前 200 字符相同而误触发。
追踪 ID 轮换不再重置计数。实质内容每次都不同的错误仍可能一直重试; 同一个规范化后的错误连续三十一次则不会。
连续性保护独立于重复次数:已经发出一次有界深度恢复的模型,再遇到资源错配时就停,
即使 maxRetries: 0 和 repeatLimit: 0 都设为不限,也不会继续扩大损失。
排查「没有重试」的原因
设一个调试日志把每次决策记录下来:
{ "resilientRetry": { "debugLog": "/tmp/resilient-retry.log" } }
只记录白名单事件 / 决策标签、次数、标志与 SHA-256 errorFingerprint。
不写原始上游错误、请求 / 响应正文、凭证、headers、日志路径。新文件权限为 0600,
不改变已有文件权限。SDK / 宿主自身的日志和原始会话记录是独立的。
{"event":"agent_before_settle","outcome":"error","stopReason":"error","aborted":false,"decision":"retry","attempt":1,"delayMs":2000,"repair":false}
{"event":"agent_before_settle","outcome":"error","stopReason":"error","decision":"skip: policy declined"}
{"event":"history_repaired","removed":3,"removedReasoning":1,"deepRepair":true,"snapshotItems":8}
{"event":"agent_before_settle","decision":"skip: continuity guard"}
{"event":"agent_before_settle","decision":"skip: unsafe replay"}
removed 统计移除的可选 ID,removedReasoning 统计舍弃的整个 reasoning 项;
deepRepair 标记修复等级,snapshotItems 表示冻结的原始前缀项数,不记录正文或项的指纹。
skip: continuity guard 表示不再做第二次有损恢复;skip: unsafe replay 表示没有安全的完整请求快照。
正常启动会记录 session_start。若缺失,请同时检查扩展加载及正确配置作用域内可写的日志路径;
日志写入失败会被忽略,不会破坏运行,不能仅凭无日志断言未加载。排查后移除 debugLog。
它不做什么
- 插件不重放任务 / 工具。 已完成工具及结果保留;模型新选择的工具调用仍交给 Pi。 失败的部分文字可能被重新生成,但真实失败响应仍保留在原始记录里。
- 不覆盖中止。 取消的运行不再重试;即使最后消息被适配器标成
error, 公共结算事件的 aborted 标志也会重置预算。 - 明确策略。 配额 / 计费错误默认排除;其他终态错误受重复 / 有限预算保护。 文案还用于判断是否允许 ID 修复。
- 不是全局拦截器。 未加载扩展的直接 provider 调用、工具 HTTP、图片生成 API 不自动覆盖。
与 Pi 内置重试的关系
顺序固定为:请求 → Pi 原生恢复 → 耗尽 / 不适用 → 插件续跑。
插件不改 retry / retry.provider,不覆盖 provider,也不新增请求内部的 stream / client 重试层。
每次续跑仍会走 Pi 原有策略。
所以 resilientRetry.maxRetries 不是总上游请求上限:原生重试 1 次、插件续跑 1 次,
连续失败时也可能产生 4 个 HTTP 请求。retry.maxRetries: 0 是关闭 Pi 原生重试,
resilientRetry.maxRetries: 0 却是插件续跑不限;请区分这两个预算。
Percho 与 SDK 的限制
本轮验证的是 Pi CLI / SDK 1.4.0 与 Percho 0.6.6;后者内置 SDK 实际导出
VERSION 1.4.0。环境中的 PI_PACKAGE_DIR 覆盖会影响 package / assets 的版本显示,
不能只按包名或旧研究材料断言运行版本。
Percho 内置子代理使用 noExtensions: true,只注入权限 factory,共享的是 ModelRuntime,
不是扩展生命周期。provider 回调发生在原生重试决策之前,公开选项没有子会话耗尽标志或
稳定逻辑请求 ID。因此没有发布透明的 native-first 子代理包装模式:它只能猜、提前重试,
或者依赖禁止的宿主私有改动。
当前 SDK 已能支持:子会话明确加载本 factory 后,现有公开 agent_before_settle 就足够实现
native-first。缺的是 Percho 的子会话加载接线,不是 SDK 重试算法。最小接入是保留
noExtensions: true 和权限 gate,白名单加载本 factory。Pi CLI 型子代理若其子进程确实加载
扩展,也可以支持;不能仅凭父会话加载就认定子会话已加载。不使用 subagent_extension:result 桥接、
不伪造 taskId,不加 UI 插件,也不修改安装包。
开发
npm install
npm test # 策略单测 + 真实 SDK / 本地 HTTP 生命周期测试,无需外网
npm run check
index.ts 只做生命周期接入;retry-core.ts 集中策略、预算、等待、修复和元数据日志。
没有新增 SDK / HTTP client 运行依赖;包只有一个明确扩展入口,并包含核心文件。
测试覆盖 SDK/client 与原生耗尽先于插件、原生成功 / Stop 不接管、配额 opt-in、有限 / 无限预算、 不重放已完成工具、旧标记身份过滤、完整错误指纹、定时器溢出 / 监听清理与安全日志。 真实 SDK + Responses / Anthropic 本地 HTTP fixture 验证图片、thinking / text 签名、工具配对、 hook 返回的替代载荷、headers、usage、不可变 ID 修复与真正的截断流。 资源检查 fixture 还验证旧状态清理后,新签名 reasoning/加密内容在后续发言及同一任务的 工具后续请求中完整保留、新工具只执行一次、再次错配会停止,以及追踪 ID 轮换和禁用修复。 单测覆盖新追加相同内容、历史前缀改写、模型/会话隔离、取消及引用型历史的安全边界。 这些是 SDK / loopback 观测,不是对真实供应商或 OAuth 的验收。
也可直接用 macOS 上 Percho 内置的 SDK 运行同一组生命周期测试(不启动桌面窗口,不修改安装):
(
ROOT=$(mktemp -d)
trap 'rm -rf "$ROOT"' EXIT
mkdir -p "$ROOT/agent"
env -i PATH="$PATH" HOME="$ROOT" PI_CODING_AGENT_DIR="$ROOT/agent" PI_OFFLINE=1 \
ELECTRON_RUN_AS_NODE=1 \
PI_RETRY_TEST_SDK=/Applications/Percho.app/Contents/Resources/app.asar/node_modules/@earendil-works/pi-coding-agent/dist/index.js \
/Applications/Percho.app/Contents/MacOS/Percho --experimental-strip-types --test test/*.test.mjs
)
工作区打包验证脚本 / 报告位于
.local/agent-work/pi-resilient-retry-optimization/package-proof/(不进入 npm 包)。
使用真实 CLI RPC 入口及两个 SDK 的普通 package / JITI loader,配置 / HOME / 会话均临时隔离,
只用假 key 和 loopback。冷加载、空闲重载、禁用 / 重启用、并行会话隔离、父会话关闭、
未注入 factory 的对照组及 Stop,与完整宿主 / 桌面验收分开报告。
没有改变真实凭证 / 配置;真实上游 OAuth、WebSocket 和 Percho 子代理原生 UI 仍未验证。
许可证
MIT