get-shit-done 3095 深度解析:Executor 子代理的配额/限流故障专属分类与恢复分支
在 get-shit-done(GSD)中,/gsd:execute-phase 会把执行计划以子代理(subagent)形式派发到 Claude Code、Copilot CLI、Codex CLI、Gemini CLI 四种宿主运行时之一。本变更(#3095)引入了一套"配额/限流终止"的专属故障分类机制:通过新的 SDK 查询 agent.classify-failure 对子代理返回的自由文本做哨兵(sentinel)匹配,产出 quota-exceeded / classify-handoff-bug / unknown-failure 三类结构化结果,并让 execute-phase 第 7 步为配额终止走"等待重置 + 恢复"而非"立即重试"的独立恢复分支。读完本文,你可以完整理解这一分类器的判定规则、哨兵优先级、retryAfterSeconds 解析方式、与 workflow 恢复提示的衔接,以及四种运行时的限流信号覆盖情况。
一、问题背景:配额终止为何需要单独的故障类
GSD 的编排器(orchestrator)派发 executor 子代理后,只能拿到一段自由文本的返回体(return body)。在 #3095 之前,除了一个特例——Claude Code 的 classifyHandoffIfNeeded is not defined 运行时 bug——所有非成功的返回体都被视为同一种"真实失败"(generic "real failure"),恢复提示会向用户提供"立即重试或中止"的选项(见 agent-failure-classifier.ts 头注释)。
但配额/限流终止(quota / rate-limit termination)与"代理崩溃"在编排器看来外观完全相同:代理中断、没有 SUMMARY.md,返回体只是一段报错文本。差异在于正确的用户响应完全不同——配额耗尽时立即重发请求会被运行时在配额窗口重置前再次拒绝,正确动作是"等待重置后再恢复(resume)"。#3095 因此把配额终止从通用失败分支中拆出,作为独立故障类处理。
二、SDK 查询入口:agent.classify-failure
该分类器注册为一条 GSD SDK 查询,标准调用形式(来自变更集 .changeset/3095-quota-failure-classification.md):
gsd-sdk query agent.classify-failure -- "<agent 返回体>"
- 返回体通过
--之后的位置参数传入。查询处理器 agentClassifyFailure 将全部位置参数以空格连接(args.join(' '))后送入分类函数,因此 workflow 里的 shell 片段可以安全地用"$AGENT_RETURN_BODY"传多行文本。 - 该查询在 command-manifest.non-family.ts 中登记为
{ canonical: 'agent.classify-failure', aliases: ['agent classify-failure'], mutation: false, outputMode: 'json' }——无副作用(mutation: false)、输出 JSON;command-static-catalog-foundation.ts 把两个名字(点号形式与空格形式)都路由到同一处理器,别名映射另见 command-aliases.generated.ts。
三、分类实现:哨兵匹配、优先级与输出契约
核心实现全部位于 sdk/src/query/agent-failure-classifier.ts,规模很小、逻辑完全确定,便于在 workflow 中直接调用。
3.1 输出契约
export type AgentFailureClass =
| 'quota-exceeded' // 配额 / 限流终止
| 'classify-handoff-bug' // Claude Code 已知运行时 bug
| 'unknown-failure'; // 兜底:通用真实失败
export interface AgentFailureClassification {
class: AgentFailureClass;
/** 命中的小写子串(若有),便于写日志 */
sentinel?: string;
/** 运行时提示的等待秒数,解析自 "retry-after: N" */
retryAfterSeconds?: number;
}
(见 类型定义。)sentinel 字段返回命中的原始哨兵小写形式,workflow 用它拼进恢复提示,让用户看到"到底哪条信号触发了配额判定"。
3.2 配额哨兵清单与运行时覆盖
配额哨兵是一个有序数组,首个命中者胜出(first match wins),因此更具体、更可操作的哨兵排在前面(见 QUOTA_SENTINELS 及顺序注释):
| 哨兵(小写) | 覆盖的运行时 / 场景 |
|---|---|
429 |
通用 HTTP 状态码;Claude Code、Codex、Gemini 的错误体均可能出现 |
usage_limit_reached |
OpenAI Codex CLI 专属哨兵 |
usage limit |
Claude Code(如 You've hit your org's monthly usage limit) |
rate limit |
通用短语(Claude Code rate_limit_error、Copilot hit a rate limit 等) |
rate-limited |
带连字符的变体 |
rate_limit |
词干(stem):一个前缀同时覆盖 rate_limited、rate_limit_error、rate_limit_exceeded 以及 Copilot 的 user_weekly_rate_limited |
resource_exhausted |
Google Gemini CLI 的 RESOURCE_EXHAUSTED |
quota |
通用词元(universal token),如 exceeded your current quota |
too many requests |
OpenAI / Codex 的 Too Many Requests(在 429 未出现时命中) |
exceeded your |
OpenAI / Gemini 的通用配额措辞 |
所有匹配均为大小写不敏感:classifyAgentFailure 先把返回体整体 toLowerCase() 再逐个 includes 检查。空串或纯空白输入直接归为 unknown-failure。
变更集里声明的"哨兵覆盖 GSD 实际派发的每种运行时"与上述清单对应:Claude Code(usage limit、429)、Copilot CLI(rate_limit、user_weekly_rate_limited)、Codex(usage_limit_reached、too many requests)、Gemini(RESOURCE_EXHAUSTED、exceeded your)。
3.3 retryAfterSeconds 解析
当提供商在返回体里回显 Retry-After 时,parseRetryAfter 用正则 /\bretry[-_ ]after[:\s]+(\d+)\b/i 提取整数秒:
- 兼容
retry-after: N、Retry-After: N、retry_after N等写法(分隔符为-、_、空格或冒号); - 只捕获整数值,HTTP-date 形式在代理返回体中很少见,作者明确不为其增加复杂度;
- 词边界
\b保证noretry-after: 3600这类嵌入词不会误报(有对应负向测试用例,见下文)。
3.4 判定优先级
优先级规则(L85-L104):
- 配额哨兵优先于
classify-handoff-bug:如果一次配额杀死了代理、同时 completion handler 又崩出了classifyHandoffIfNeeded is not defined,返回体里两个哨兵都会出现——此时它仍是配额事件(等待 vs 抽查后视为成功的恢复路径不同),分类器返回quota-exceeded; - 只有配额哨兵全部未命中、且文本不含
classifyhandoffifneeded is not defined时,才落到unknown-failure。
四、execute-phase 第 7 步:三分支恢复路由
分类结果被消费的位置是 get-shit-done/workflows/execute-phase.md 的 "Handle failures" 环节(Step 7.0–7.3)。
4.1 Step 7.0 — 先分类再分支
workflow 在分支前先用一段 shell 把返回体过一遍分类器(原文片段,可直接复制):
CLASS_JSON=$(gsd-sdk query agent.classify-failure -- "$AGENT_RETURN_BODY")
CLASS=$(echo "$CLASS_JSON" | jq -r '.class')
SENTINEL=$(echo "$CLASS_JSON" | jq -r '.sentinel // empty')
RETRY_AFTER=$(echo "$CLASS_JSON" | jq -r '.retryAfterSeconds // empty')
if [ -n "$RETRY_AFTER" ]; then RETRY_HINT=" Provider hinted retry-after: ${RETRY_AFTER}s"; else RETRY_HINT=""; fi
一段分类分支即可横跨 Claude / Copilot / Codex / Gemini 四种运行时;workflow 中还显式指向研究笔记 docs/research/provider-rate-limit-signals.md 作为信号层的参考。
4.2 Step 7.1 — quota-exceeded:不重试,等重置再恢复
这是本次变更的核心行为变化:恢复提示不再提供"立即重试"(运行时会在配额窗口重置前再次拒绝),而是先做第 5 步的 spot-check(核对 SUMMARY.md 与 worktree 分支上的部分提交);若 SUMMARY.md 缺失但提交已存在,则走 safe-resume 路径(state.verify-against-disk)而不是立即重新派发。恢复提示模板(workflow 原文):
⚠ Plan {plan_id} terminated by provider quota / rate limit
Runtime sentinel: {SENTINEL}
{RETRY_HINT}
Partial commits on worktree branch: {N}
SUMMARY.md present: {yes|no}
1. Wait for quota reset, then resume (recommended)
2. Switch to a different runtime / model and resume
3. Abort phase and report partial state
选项 1 的执行方式是配额重置后重新运行 /gsd:execute-phase。变更集同时说明:这条 resume 路径本身依赖 safe-resume 门禁(在 #3212 中落地)的 execute-phase 卡死安全恢复能力,即恢复机制与既有断点续跑体系(STATE.md 记录最后完成的 plan / 当前 wave / 待处理 checkpoint)衔接。
4.3 Step 7.2 / 7.3 — 其余两类
classify-handoff-bug:错误体含classifyHandoffIfNeeded is not defined时按 Claude 运行时 bug 处理;跑同样的 spot-check——PASS 则视为成功,FAIL 则落入通用失败处理。这个既有特例被并入同一分类器后,Step 7 有了单一的分发点(single dispatch point);unknown-failure:报告失败 plan 并询问 Continue/Stop;继续执行可能引发依赖链上后续 plan 的级联失败(wave 依赖关系)。
workflow 尾部的 failure_handling 段 也同步更新,声明了跨运行时的配额哨兵与"不重试、等重置"规则;resumption 段 说明恢复机制:重跑 /gsd:execute-phase {phase} 后 discover_plans 会找到已完成的 SUMMARY 并跳过,从第一个未完成的 plan 续跑——这正是"等待配额重置后恢复"能够无状态地工作的前提。
五、测试:判定边界与负向用例的分类验证
单元测试 agent-failure-classifier.test.ts 把每个运行时变体和边界情况都固化为可执行断言,选摘几个关键用例:
| 返回体样例 | 期望 class / sentinel | 验证点 |
|---|---|---|
You've hit your org's monthly usage limit |
quota-exceeded / usage limit |
#3095 报告中的原始组织级月限额文案 |
Request failed: HTTP 429 Too Many Requests |
quota-exceeded / 429 |
429 优先于 too many requests(更具体) |
agent failed: usage_limit_reached |
quota-exceeded / usage_limit_reached |
Codex CLI 哨兵 |
Server Error: user_weekly_rate_limited |
quota-exceeded(sentinel 有定义即可) |
Copilot 周限额,经 rate_limit 词干命中 |
Error: RESOURCE_EXHAUSTED — You exceeded your current quota |
quota-exceeded |
Gemini 哨兵;quota 与 resource_exhausted 谁先命中均可接受 |
USAGE LIMIT REACHED |
quota-exceeded / usage limit |
大小写不敏感 |
rate_limit_error: retry-after: 3600 |
retryAfterSeconds: 3600 |
整数秒提取 |
Failed: 429 Too Many Requests (Retry-After: 60) |
retryAfterSeconds: 60 |
头式值回显也能解析 |
noretry-after: 3600 quota exceeded |
class 正确,但 retryAfterSeconds 为 undefined |
词边界负向用例 |
ReferenceError: classifyHandoffIfNeeded is not defined |
classify-handoff-bug |
既有特例保持可调用 |
配额文案 + classifyHandoffIfNeeded 并存 |
quota-exceeded |
优先级:配额压过运行时 bug |
| 空串 / 纯空白 / 通用报错 | unknown-failure |
兜底分支 |
(测试用例见 agent-failure-classifier.test.ts。)
六、前瞻:从"事后分类"到"事前预警"
provider-rate-limit-signals.md 记录了各提供商限流信号的三层结构——HTTP 头、SDK 事件、事后错误体——并指出当前宿主运行时只把"事后错误体"这一层暴露给 GSD:Claude Code 不向 hooks 转发 anthropic-ratelimit-* / retry-after 头,Copilot 的前置警告只出现在子进程 stdout,Codex 的 x-ratelimit-remaining-* 头同样未转发到 hooks。因此在宿主运行时把主动信号暴露到 hooks 之前,agent.classify-failure 就是当前可操作的能力边界:事后错误体哨兵 + execute-phase Step 7 的配额专属恢复提示。
该研究笔记同时给出了信号打通后的演进方向("从源码结构看"属于计划路径,非当前已实现能力):
- 在 PreToolUse / SessionStart hook 中读取
requests-remaining/tokens-remaining,当值低于可配置阈值时,在派发新 wave 前向用户做软警告; - 把 Anthropic Agent SDK 的
RateLimitEvent.status == "allowed_warning"作为长时 executor 的 checkpoint 信号——先产出部分 SUMMARY,让编排器在重置后接续; - 与
executor.stall_threshold_minutes(#3329)联动,让编排器不必等满卡死检测间隔,在运行时已发出rejected时即转入恢复路径。
七、小结
#3095 用一个约百行的纯函数分类器 + 一处 workflow 分支,解决了多运行时编排中的一个真实痛点:把"配额耗尽"从"代理崩了"里识别出来,让用户收到"等重置、再恢复"而非"再试一次"的正确指引。关键落点:
- 分类器:sdk/src/query/agent-failure-classifier.ts(哨兵有序匹配、
retryAfterSeconds解析、优先级规则); - 查询注册:sdk/src/query/command-manifest.non-family.ts、sdk/src/query/command-static-catalog-foundation.ts;
- 恢复路由:get-shit-done/workflows/execute-phase.md 的 Step 7.0–7.3 与 failure_handling / resumption 段;
- 测试与信号研究:sdk/src/query/agent-failure-classifier.test.ts、docs/research/provider-rate-limit-signals.md;
- 变更说明:.changeset/3095-quota-failure-classification.md。
适用前提:agent.classify-failure 依赖已安装的 GSD SDK(gsd-sdk query 可用);哨兵清单面向的是 GSD 当前派发的四种宿主运行时,若接入其他运行时,可参照 QUOTA_SENTINELS 的有序数组自行追加对应错误体措辞。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00