Skip to content

bug: Claude Code session-limit 429 被判为 TRANSIENT 却无人重试 — 不透出 reset 时间、丢弃子代理已完成成果,并被无上限并行扇出放大 #127

Description

@johnsonice

What happened?

Claude Code 作为 provider 时频繁出现 "rate limit" 打断 agent turn。用户去查 Claude Code 的 usage 页面,完全没有到限额 —— 因此以为是 NarraNexus 误报。

实测从本地 DB 挖出原始错误(events.event_log),429 是真的,但它不是用户以为的那种 rate limit

due to an API error: You've hit your session limit · resets 3:20am (America/New_York)
(error type rate_limit, HTTP 429, request id req_011CeYACYtmED5ZgqkdBEwvY,
 model sent to the API: claude-opus-5)

Everything below is PARTIAL output recovered from the agent before ...

这是 Claude Code 的 5 小时 session 额度,Anthropic 用 error type rate_limit / HTTP 429 的外壳返回 —— 和 usage 页面展示的不是同一个计量,所以用户查不到。NarraNexus 没有误报

真正的缺陷在于:这个错误进来之后,平台既不重试、也不告诉用户发生了什么、还丢掉了已完成的工作。同一次 turn 里有 3 个不同 request id 连续撞上(多个研究子代理并发),用户看到的只是 agent 用自然语言转述一句"被 API 速率限制打断了"。

仓库核对(dev @ c776e32a / v1.15.0):

1. 归类为 TRANSIENT,然后没有任何东西重试它

loop/circuit_breaker.py:99-104_TRANSIENT_MARKERS"rate limit" / "429"classify_agent_error:131-176)返回 ErrorCategory.TRANSIENT
TRANSIENT ∉ PAUSING_CATEGORIESschema/agent_circuit_breaker_schema.py:67-69 只含 AUTH/QUOTA),所以不会熔断 —— 这点是对的,与 #117 无关。
但 grep 全仓 TRANSIENT 的消费方只有 wechat_trigger / model_sync / logging/_ship.pyagent turn 路径一个都没有。也就是说:系统明确判定"这是暂时性的、重试可恢复",然后从不重试,turn 直接以残缺输出结束。

2. 错误里带着 reset 时间,但被丢弃

原文含机器可读的 resets 3:20am (America/New_York)。平台没有解析它,因此无法做退避重试、无法排程恢复、也无法在 UI 上告诉用户"几点恢复"。

3. failure.py 对 session limit 零识别 → 用户拿不到可操作指引

agent_framework/llm/failure.py 的自助错误分类(SELF_SERVICEABLE_REASON_*)里 没有 rate_limit / session limit 这一类(grep session limit|rate_limit|429|resets = 0 命中)。所以 classify_self_serviceable 返回 None,用户得不到类似 "已用完 5 小时 session 额度,3:20am 恢复" 的明确提示 —— 只能看 agent 的自然语言转述,然后去 usage 页面白查一场。

对比:同文件对 insufficient_balance / free_tier_exhausted / context_window / model_not_found 都有精确 marker + 可操作文案。session limit 是 Claude Code provider 下最常撞的一种,反而缺席。

4. 子代理扇出无并发上限 —— 这是放大器

adapters/claude/sdk.py:876-925permission_mode="bypassPermissions"max_turns=None、只在条件成立时禁 WebSearch没有 agents= 配置
grep semaphore|max_concurrent|concurrency 在整个 adapters/claude/ + loop/ 路径下 零命中;grep 429|backoff|RateLimitadapters/claude/ 下也只有 stale-resume 的 cold retry(sdk.py:1293-1363,明确限定"至多一次、且只在有输出之前",不针对 429)。

结果:模型一次 turn 里想扇出多少研究子代理就扇出多少,每个都是独立的 API 流、都在烧同一个 5 小时 session 预算,而且用的是 claude-opus-5窗口被一次并行调研打穿,正是用户"明明没怎么用却频繁撞限"的体感来源。

5. 已完成的部分成果被丢弃

Claude Code 自己回收了部分输出(错误文本里的 Everything below is PARTIAL output recovered from the agent before ...),但 NarraNexus 既没有把这一轮标记为 incomplete,也没有提供 resume/续跑入口。多个子代理已经交付的中间成果就这么没了。

Steps to reproduce

  1. dev 分支(本次为 c776e32a / v1.15.0),provider 选 Claude Code,模型 claude-opus-5
  2. 给 agent 一个需要大量并行调研的任务,让它扇出多个研究子代理
  3. 跑几轮,直到 Anthropic 5 小时 session 额度耗尽
  4. 观察:agent 用自然语言说"被 API 速率限制打断了",turn 以残缺输出结束;无重试、无恢复时间提示、无续跑入口
  5. 去 Claude Code usage 页面查 —— 看不到任何超限(因为那是另一套计量)

NarraNexus version

v1.15.0(dev @ c776e32a

How are you running NarraNexus?

本地源码 / bash run.sh(macOS)

Logs / screenshots

原始错误取自本地 ~/.narranexus/nexus.dbevents.event_log(event 时间 2026-08-30 02:21:49agent_00f08eaaef23),同一 turn 内三条:

... due to an API error: You've hit your session limit · resets 3:20am (America/New_York)
    (error type rate_limit, HTTP 429, request id req_011CeYACYtmED5ZgqkdBEwvY, model sent to the API: claude-opus-5)
... (同上, request id req_011CeYACZAe7JfiKbEFyypDp)
... (同上, request id req_011CeYACaqr4T6S5f6BGbRVi)

用户侧看到的(agent 自然语言转述,非平台提示):

这轮调研我进行了大量并行研究,但中途被 API 速率限制打断了——多个研究代理提交了部分成果后就断了。

Anything else?

建议修复(按性价比排序):

  1. 给 session-limit 429 一个专属分类 + 可操作文案。failure.pySELF_SERVICEABLE_REASON_SESSION_LIMIT,marker 用 "session limit"(配合 error type rate_limit),文案直接把错误里的 reset 时间透出来:"Claude Code 5 小时 session 额度已用完, 恢复"。这样用户不会再去 usage 页面白查。
    注意别撞 classify_agent_error 的既有排序陷阱 —— 该函数 docstring 明确说 QUOTA 分支刻意不看 "429"/"rate limit",新增 reason 要保持这条不变(session limit 不是余额问题,remedy 是等待而非充值)。

  2. 让 TRANSIENT 在 agent turn 路径上真的有人管。 现在"判定为可重试"和"实际重试"之间是断的。至少解析 reset 时间做一次排程续跑;铁律 weekly #14 下不要做无上限轮询,按 reset 时间单次唤醒即可。

  3. 给子代理扇出加并发上限。 这是根治"窗口被一次并行调研打穿"的地方。可以是固定上限,也可以按 provider 区分(Claude Code 的 session 预算比 API key 计费更容易打穿)。

  4. 保住残缺成果。 Claude Code 已经把 partial output 交回来了,把这一轮标记为 incomplete 并给个续跑入口,别让多个子代理的中间成果白跑。

关联:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaves unexpectedlyneeds-triageAwaiting maintainer review for next step

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions