@yanzc/claude-pro 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/package.json +1 -1
- package/src/agent.js +25 -4
- package/src/agents.js +5 -0
- package/src/api.js +15 -9
- package/src/config.js +21 -11
- package/src/context.js +4 -1
- package/src/hooks.js +15 -4
- package/src/print.js +2 -2
- package/src/ratelimit.js +24 -0
- package/src/runtime.js +6 -2
- package/src/tools/files.js +18 -8
- package/src/tools/search.js +8 -0
- package/src/tools/task.js +12 -3
- package/src/ui/editor.js +25 -5
- package/src/ui/repl.js +6 -6
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ claude-pro config set CLAUDE_PRO_PROVIDER claude-code
|
|
|
64
64
|
|
|
65
65
|
调用方式是:本项目 → `https://api.anthropic.com/v1/messages?beta=true`。请求复用 `Authorization: Bearer <accessToken>`,带上 Claude Code 的 OAuth beta、`x-app: cli`、User-Agent、系统标识,以及来自 `.claude.json` 的设备和账号 metadata。流式响应、原生 `tool_use` / `tool_result`、工具执行、hooks、检查点、计划模式及会话功能都由本项目原有逻辑处理。
|
|
66
66
|
|
|
67
|
-
此模式默认 `sonnet` → `claude-sonnet-5-5`,`opus` → `claude-opus-5-5`,`haiku` → `claude-haiku-
|
|
67
|
+
此模式默认 `sonnet` → `claude-sonnet-5-5`,`opus` → `claude-opus-5-5`,`haiku` → `claude-haiku-5-5`(与 Claude Code 2.1.293 起一样;话题标题、WebFetch 摘要等小请求用它);子 agent 默认用 `sonnet`(`CLAUDE_PRO_CLAUDE_CODE_SUBAGENT_MODEL` 可改)。使用 `--model`、`/model` 或 `CLAUDE_PRO_CLAUDE_CODE_MODEL` 指定模型。**`/model`、`/effort` 的选择会一直保留**:按后端记在 `~/.claude-pro/settings.json` 的 `remembered` 里,之后每次启动(交互式和 `-p`)都沿用,优先于 `.env`/settings/环境变量里的配置;命令行 `--model`/`--effort` 只覆盖当次启动;`/model default`、`/effort default` 删掉记住的选择,回到配置的值。`claude-pro config list` 会显示当前记住的选择。已有的 DeepSeek API URL、API key 和 API 模型别名不会参与订阅请求。OAuth 令牌始终只发往 Anthropic,原 API 配置可以通过 `--provider api` 继续使用。
|
|
68
68
|
|
|
69
69
|
每次请求重新读取现有凭据;如果本机 Claude Code 更新了令牌,下次请求自动使用新值。
|
|
70
70
|
|
|
@@ -116,13 +116,13 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
116
116
|
每次请求都会把整个对话重新发给模型,账单的大头是"重复读历史"。本项目按 [Claude Code 的做法](https://code.claude.com/docs/en/prompt-caching)使用 Anthropic 的提示词缓存:
|
|
117
117
|
|
|
118
118
|
- **断点**:在工具列表末尾、系统提示词、最新一条消息的末尾各放一个 `cache_control`(共 3 个,上限 4 个)。系统提示词在一个会话里只构建一次,前缀稳定才能命中。
|
|
119
|
-
- **开箱即用,不需要配置**:默认值和 Claude Code 一致。订阅登录(`--provider claude-code`)下**主对话用 1 小时**缓存(Claude Code
|
|
119
|
+
- **开箱即用,不需要配置**:默认值和 Claude Code 一致。订阅登录(`--provider claude-code`)和网关(`gateway`)下**主对话用 1 小时**缓存(Claude Code 在订阅额度内就是这样请求的;网关按 2 倍价计费 1 小时写入,但按网关的用量记录算,533 次 Opus 请求里停顿 5–60 分钟让整段对话重写了 9 次、多花 $14.77,全部改用 1 小时只多花 $2.80),API key / 其他端点用 5 分钟(API 默认值,不发送 `ttl` 字段);子 agent、压缩等其他请求永远默认 5 分钟。effort 不设就用模型自己的默认值(Sonnet 5.5、Opus 5.5 是 medium),自动压缩窗口不设就是模型的整个窗口。
|
|
120
120
|
- **`.env` 只属于 API 提供方**:`--provider claude-code` 时,`.env` 里的 `CLAUDE_CODE_EFFORT_LEVEL`、`CLAUDE_CODE_AUTO_COMPACT_WINDOW`、`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`、`CLAUDE_CODE_MAX_OUTPUT_TOKENS`、`MAX_THINKING_TOKENS` 不会被读取(它们是为 DeepSeek 之类的端点调的),订阅模式用上面的默认值;`/config` 会列出被忽略的键。想在订阅模式下调,用 `/config set`、`/effort`、设置文件、环境变量或 `--effort`,这些照常生效。
|
|
121
121
|
- **TTL 分两个桶**(可以改),每次命中会重新计时:
|
|
122
122
|
- **主对话**(你的每一轮):`CLAUDE_CODE_PROMPT_CACHE_TTL=5m|1h`,或设置项 `promptCacheTtl`
|
|
123
123
|
- **其他请求**(子 agent、压缩、小模型的辅助请求):`CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL=5m|1h`,或设置项 `subagentPromptCacheTtl`
|
|
124
124
|
- 变量优先于设置项(无论来自哪个文件);无效的值被忽略,按下一条规则算。`ENABLE_PROMPT_CACHING_1H=1` 让两个桶都用 1 小时(上面两个变量和设置项优先);`FORCE_PROMPT_CACHING_5M=1` 强制两个桶都用 5 分钟,优先级最高;其他取值被忽略。
|
|
125
|
-
- 1 小时档的缓存写入按 2 倍价格计费(5 分钟档是 1.25 倍),而且每一轮新增的内容都按这个价写:用 API key 按 token 付费时,只有上下文很大、而且常常停顿超过 5 分钟才划算,所以那里默认是 5 分钟。订阅额度内 Claude Code 默认用 1
|
|
125
|
+
- 1 小时档的缓存写入按 2 倍价格计费(5 分钟档是 1.25 倍),而且每一轮新增的内容都按这个价写:用 API key 按 token 付费时,只有上下文很大、而且常常停顿超过 5 分钟才划算,所以那里默认是 5 分钟。订阅额度内 Claude Code 默认用 1 小时;想在订阅模式或网关下用 5 分钟,设 `FORCE_PROMPT_CACHING_5M=1`。
|
|
126
126
|
- **关闭缓存**:`DISABLE_PROMPT_CACHING=1` 完全不发 `cache_control`;`DISABLE_PROMPT_CACHING_HAIKU` / `_SONNET` / `_OPUS` 只关 haiku/sonnet/opus 别名所指的模型,`_FABLE` 只关 Fable 模型。
|
|
127
127
|
- **端点不认**:网关对 `cache_control`(或 `ttl`)返回 400 时,不会让请求失败:在被点名的位置去掉标记(报 `ttl` 则退回 5 分钟)后重发;没点名位置的报错一次去掉一处(先消息、再工具、再系统提示词),尽量多保留缓存。请求成功之后才记住这个端点拒绝了什么(之后不再发送),`/status` 会显示;报错本身只是提到 `cache_control` 而去掉标记也没用时,什么都不会记住。
|
|
128
128
|
- **一次性的请求不加缓存标记**:WebFetch 摘要这类整页内容只发一次的小模型请求,缓存写入只会多收钱,所以不标记。
|
|
@@ -151,6 +151,7 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
151
151
|
- **压缩对话时**撞上限流同样会暂停,界面显示同样的倒计时(不会一直停在"Compacting conversation"上让人以为卡死了)。
|
|
152
152
|
- **子 agent 和后台 agent** 同样会等待,暂停与恢复都以 `[agent 名]` 为前缀提示一次(倒计时那一行属于主 agent,因为后台 agent 暂停时主 agent 可能还在干活)。轮询(服务器没给重置时间)每隔几分钟重试一次,但只提示一次,不会刷屏。
|
|
153
153
|
- `/doctor` 这类诊断请求不等,直接报告限流。
|
|
154
|
+
- **API 过载(529 / `overloaded_error`)** 也不会让任务停下:先按 `CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS`(默认 1 秒)起、每次翻倍、最长 `CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS`(默认 32 秒)快速重试 `CLAUDE_PRO_MAX_RETRIES` 次;用完后转为暂停(界面显示 `API overloaded · checking again in …`,Esc 取消),每隔最长间隔再试一次,直到 API 恢复(提示 `The API is answering again; continuing.`),最多等 `CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS`(默认同 `CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS`,6 小时;`0` = 重试用完就报错)。
|
|
154
155
|
|
|
155
156
|
## 终端标题、页脚与窗口缩放
|
|
156
157
|
|
|
@@ -186,7 +187,7 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
186
187
|
| Bash 默认 / 最长超时 | 2 分钟 / 10 分钟 | 10 分钟 / 1 小时 |
|
|
187
188
|
| Bash 输出返回给模型的上限 | 30000 字符 | 60000 字符,超出部分的全文保存到文件 |
|
|
188
189
|
| 单个工具结果上限(超出落盘) | 50000 字符 | 100000 字符 |
|
|
189
|
-
| `Read` 一次最多 | 2000 行、每行 2000 字符、总共 12 万字符 | 10000 行、每行 20000 字符、总共 40
|
|
190
|
+
| `Read` 一次最多 | 2000 行、每行 2000 字符、总共 12 万字符 | 10000 行、每行 20000 字符、总共 40 万字符;加 `allow_large: true`(与 Claude Code 2.1.296 相同)不限行数和长度,只要上下文在自动压缩之前还放得下(同一批并行读取共用这份余量),放不下就拒绝并写明差多少 |
|
|
190
191
|
| `Glob` / `Grep` 默认返回条数 | 100 / 250 | 1000 / 1000 |
|
|
191
192
|
| `WebFetch` | 只返回小模型的摘要(最多 4096 token),自动把 http 升成 https | `raw: true` 直接拿转换后的全文;摘要上限 16000 token、页面上限 40 万字符;按原样请求 URL,localhost 和内网也可以 |
|
|
192
193
|
| 子 agent | 不能再派子 agent | 嵌套到 2 层(`CLAUDE_PRO_MAX_SUBAGENT_DEPTH`,0 = 不嵌套) |
|
|
@@ -197,12 +198,13 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
197
198
|
|
|
198
199
|
## 功能
|
|
199
200
|
|
|
200
|
-
**工具**(与 Claude Code 同名同语义):`Bash`(持久 cwd、超时、后台运行、可喂 stdin)、`BashOutput`、`KillShell`、`Read`(行号、图片、ipynb)、`Write`、`Edit`(必须先读后改、检测外部修改、保留 CRLF/BOM)、`NotebookEdit`、`Glob`、`Grep`(有 ripgrep 用 rg,否则用内置 JS 搜索:异步不卡界面,遵循 `.gitignore`/`.ignore`、`!` 排除 glob、`^`/`$` 行锚点、多行匹配、上下文行;找不到 rg 时可用 `CLAUDE_PRO_RIPGREP_PATH` 指定;与 rg 不同的是它始终跳过 `node_modules` 等目录,除非 glob
|
|
201
|
+
**工具**(与 Claude Code 同名同语义):`Bash`(持久 cwd、超时、后台运行、可喂 stdin)、`BashOutput`、`KillShell`、`Read`(行号、图片、ipynb)、`Write`、`Edit`(必须先读后改、检测外部修改、保留 CRLF/BOM)、`NotebookEdit`、`Glob`、`Grep`(有 ripgrep 用 rg,否则用内置 JS 搜索:异步不卡界面,遵循 `.gitignore`/`.ignore`、`!` 排除 glob、`^`/`$` 行锚点、多行匹配、上下文行;找不到 rg 时可用 `CLAUDE_PRO_RIPGREP_PATH` 指定;与 rg 不同的是它始终跳过 `node_modules` 等目录,除非 glob 里点名;模型误传的 `file_path` 当作 `path`,grep 的 `-l`/`-c` 当作输出模式)、`WebFetch`、`WebSearch`(DuckDuckGo/Bing)、`TodoWrite`、`Task`(子 agent)、`TaskOutput`、`TaskStop`、`Skill`、`ExitPlanMode`、`AskUserQuestion`,以及 MCP 工具。
|
|
201
202
|
|
|
202
203
|
**交互**
|
|
203
204
|
- 流式 Markdown 渲染(代码高亮、表格、中文宽度对齐),思考过程折叠显示(`ctrl+o` 展开)
|
|
204
205
|
- `Esc` 中断;忙碌时输入的消息会排队并插入当前任务
|
|
205
206
|
- `!` Bash 模式、`#` 写入记忆(CLAUDE.md)、`\`+回车 或 `ctrl+j` 换行、粘贴大段文本折叠为 `[Pasted text #1 +42 lines]`
|
|
207
|
+
- `ctrl+c` 或连按两次 `Esc` 清掉的草稿不会丢:在空输入框按 `↑` 找回(连同粘贴的文本和图片),中间发过别的消息也可以
|
|
206
208
|
- `@文件` 补全并附加内容:走的就是 `Read` 工具,等于模型自己读了这个文件。最多带前 2000 行(被截断时会明确告诉模型,并把该文件记为"只读了一部分",之后 `Write` 不会去覆盖没给它看过的行);图片作为图片附加;二进制文件和超过 512KB 的文本会被跳过并提示原因;`@目录` 列出目录内容
|
|
207
209
|
- `alt+v` 粘贴剪贴板里的图片(截图用 `Win+Shift+S`),输入框里显示为 `[Image #1]`;超过 1568px 的图会自动缩小。把图片文件拖进终端(或粘贴图片路径)也会作为图片附加
|
|
208
210
|
- 上下文用量接近阈值时自动压缩(`CLAUDE_CODE_AUTO_COMPACT_WINDOW`),也可手动 `/compact`
|
|
@@ -210,13 +212,13 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
210
212
|
|
|
211
213
|
**斜杠命令**:`/help` `/clear` `/compact` `/config` `/model` `/effort` `/status` `/cost` `/context` `/init` `/review` `/memory` `/verify` `/tasks` `/plan` `/rewind` `/skills` `/resume` `/todos` `/agents` `/mcp` `/bashes` `/verbose` `/export` `/doctor` `/exit`,以及 `.claude/commands/*.md`、`.claude-pro/commands/*.md`、`~/.claude-pro/commands/*.md` 中的自定义命令(支持 `$ARGUMENTS`、`$1`、`` !`cmd` ``)。
|
|
212
214
|
|
|
213
|
-
**记忆**:自动读取 `~/.claude-pro/CLAUDE.md`、项目各级目录的 `CLAUDE.md` / `.claude/CLAUDE.md` / `
|
|
215
|
+
**记忆**:自动读取 `~/.claude-pro/CLAUDE.md`、项目各级目录的 `CLAUDE.md` / `.claude/CLAUDE.md` / `CLAUDE.local.md`;某一级目录没有 `CLAUDE.md` 时读它的 `AGENTS.md` 代替(与 Claude Code 相同,两者都有时通常是同一份说明,不重复发送)。
|
|
214
216
|
|
|
215
217
|
**自动记忆(跨会话)**:每个项目有一个由 agent 自己维护的记忆目录 `~/.claude-pro/projects/<项目>/memory/`。它会把你的偏好、纠正过的做法、项目约束等写成一条条小文件,并在 `MEMORY.md` 里建索引;每次新会话开始时索引自动载入。直接说"记住……"或"忘掉……"即可;`/memory` 查看;`CLAUDE_PRO_DISABLE_AUTO_MEMORY=1` 关闭。
|
|
216
218
|
|
|
217
219
|
**Skills**:`<目录>/skills/<名字>/SKILL.md`,frontmatter 写 `name` 和 `description`,正文是指令,格式与 Claude Code 的 skills 相同。搜索 `~/.claude-pro/skills/`、`<项目>/.claude/skills/`、`<项目>/.claude-pro/skills/`(项目级覆盖用户级)。模型发现任务匹配某个 skill 的描述时会通过 `Skill` 工具自动加载它;你也可以直接用 `/<skill 名> [参数]` 调用,`/skills` 列出全部。
|
|
218
220
|
|
|
219
|
-
**子 agent**:内置 `general-purpose`、`Explore`、`Plan`;自定义 agent 放在 `.claude/agents/*.md` 或 `~/.claude-pro/agents/*.md`(frontmatter:`name`、`description`、`tools`、`model
|
|
221
|
+
**子 agent**:内置 `general-purpose`、`Explore`、`Plan`;自定义 agent 放在 `.claude/agents/*.md` 或 `~/.claude-pro/agents/*.md`(frontmatter:`name`、`description`、`tools`、`model`、`effort`(low/medium/high/xhigh/max)、`autoCompactWindow`(这个 agent 自己的自动压缩窗口,token 数))。模型调用 `Task` 时也可以给单个子 agent 指定 `model`(sonnet/opus/haiku 或完整 id)和 `effort`,优先于 agent 定义里的,定义里的又优先于会话的。
|
|
220
222
|
|
|
221
223
|
**后台子 agent**:模型可以用 `Task` 的 `run_in_background: true` 把子 agent 放到后台,自己继续干别的。子 agent 完成后,结果会作为 `<task-notification>` 自动送回;如果当时主 agent 空闲,会自动开启新的一轮来处理结果。`TaskOutput` 用于提前等待结果,`TaskStop` 用于取消;`/tasks` 列出所有后台 agent,`/tasks kill agent_1` 停止其中一个。`-p` 模式会等所有后台 agent 结束后再退出。
|
|
222
224
|
|
|
@@ -224,7 +226,7 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
224
226
|
|
|
225
227
|
**大输出落盘**:工具结果超过 `CLAUDE_PRO_MAX_TOOL_RESULT_CHARS`(默认 50000 字符)时,全文会保存到 `~/.claude-pro/projects/<项目>/tool-results/`,只把预览和文件路径发给模型,模型可以再用 `Read`/`Grep` 按需查看。Bash 输出被截断时同样会保存全文。超过 7 天的旧文件在启动时自动清理。
|
|
226
228
|
|
|
227
|
-
**Hooks**:`settings.json` 里的 `hooks` 与 Claude Code 同形:`PreToolUse`、`PostToolUse`、`UserPromptSubmit`、`Stop`。事件内容以 JSON 从 **stdin** 传给脚本(同时放在环境变量 `CLAUDE_PRO_HOOK_INPUT`,另有 `CLAUDE_PROJECT_DIR`);退出码 0 = 通过(stdout 作为附加上下文),2 = 拦截(stderr 反馈给模型;`Stop` 则让模型继续干活),其他 =
|
|
229
|
+
**Hooks**:`settings.json` 里的 `hooks` 与 Claude Code 同形:`PreToolUse`、`PostToolUse`、`UserPromptSubmit`、`Stop`。事件内容以 JSON 从 **stdin** 传给脚本(同时放在环境变量 `CLAUDE_PRO_HOOK_INPUT`,另有 `CLAUDE_PROJECT_DIR`);退出码 0 = 通过(stdout 作为附加上下文),2 = 拦截(stderr 反馈给模型;`Stop` 则让模型继续干活),其他 = 仅警告。当护栏用的 hook 加上 `"onFailure": "block"`:启动不了、超时或以其他退出码结束时按拦截处理,而不是放行(与 Claude Code 相同)。hook 输出里的 `<system-reminder>` 标签会被转义,冒充不了系统提示。
|
|
228
230
|
|
|
229
231
|
```json
|
|
230
232
|
{ "hooks": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanzc/claude-pro",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/agent.js
CHANGED
|
@@ -168,8 +168,10 @@ function validateInput(schema, input) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
export class Agent {
|
|
171
|
-
constructor({ config, modes, tools, systemPrompt, io, model, isSubagent = false, cwd, name = 'main', spawnSubagent, hooks, tasks, checkpoints, depth = 0, refreshSystemPrompt, beforeTurn }) {
|
|
171
|
+
constructor({ config, modes, tools, systemPrompt, io, model, isSubagent = false, cwd, name = 'main', spawnSubagent, hooks, tasks, checkpoints, depth = 0, refreshSystemPrompt, beforeTurn, effort, compactWindow = 0 }) {
|
|
172
172
|
this.config = config;
|
|
173
|
+
this.effort = effort || undefined; // a sub-agent's own effort level; undefined: the session's (config.effort)
|
|
174
|
+
this.compactWindow = compactWindow; // a sub-agent's own auto-compact window (tokens); 0: the session's
|
|
173
175
|
this.depth = depth; // 0 for the main agent, +1 for every level of sub-agent
|
|
174
176
|
this.hooks = hooks;
|
|
175
177
|
this.tasks = tasks; // background tasks (sub-agents started with run_in_background)
|
|
@@ -191,6 +193,7 @@ export class Agent {
|
|
|
191
193
|
this.cwd = cwd;
|
|
192
194
|
this.lastContextTokens = 0;
|
|
193
195
|
this.tokensSinceUsage = 0;
|
|
196
|
+
this.heldTokens = 0; // room taken by tool results not yet in the conversation (see takeContext)
|
|
194
197
|
// What this agent's own requests used. `requests` counts the turns' requests; the summary requests of
|
|
195
198
|
// compactions are counted apart (`compactions`) and their tokens are in the totals like any others.
|
|
196
199
|
this.usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0, requests: 0, compactions: 0 };
|
|
@@ -216,9 +219,26 @@ export class Agent {
|
|
|
216
219
|
|
|
217
220
|
get contextEstimate() { return this.lastContextTokens + this.tokensSinceUsage; }
|
|
218
221
|
|
|
222
|
+
// Tokens the context can still take before it reaches the auto-compaction threshold: what a Read with allow_large
|
|
223
|
+
// may use (a larger one would be compacted away right after it was read).
|
|
224
|
+
contextRoom() {
|
|
225
|
+
return this.config.autoCompactThreshold(this.model, this.compactWindow) - this.contextEstimate - this.heldTokens;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Claim room for a tool result that is about to be added. Read-only tools run in parallel, so what each one
|
|
229
|
+
// took is held until their results are in the conversation. Returns {ok, tokens, room}.
|
|
230
|
+
takeContext(value) {
|
|
231
|
+
const tokens = estimateTokens(value);
|
|
232
|
+
const room = this.contextRoom();
|
|
233
|
+
if (tokens > room) return { ok: false, tokens, room };
|
|
234
|
+
this.heldTokens += tokens;
|
|
235
|
+
return { ok: true, tokens, room };
|
|
236
|
+
}
|
|
237
|
+
|
|
219
238
|
pushMessage(msg) {
|
|
220
239
|
this.messages.push(msg);
|
|
221
240
|
this.lastActivityAt = Date.now();
|
|
241
|
+
if (msg.role === 'user') this.heldTokens = 0; // the results that held room are counted from here on
|
|
222
242
|
if (msg.role === 'user') this.tokensSinceUsage += estimateTokens(msg.content);
|
|
223
243
|
this.io.messageAdded?.(msg, this);
|
|
224
244
|
}
|
|
@@ -473,7 +493,7 @@ export class Agent {
|
|
|
473
493
|
shouldAutoCompact() {
|
|
474
494
|
if (!this.config.autoCompactEnabled || this.messages.length <= 2) return false;
|
|
475
495
|
if (this.compactFailures >= MAX_COMPACT_FAILURES) return false;
|
|
476
|
-
const threshold = this.config.autoCompactThreshold(this.model);
|
|
496
|
+
const threshold = this.config.autoCompactThreshold(this.model, this.compactWindow);
|
|
477
497
|
if (this.contextEstimate < threshold) return false;
|
|
478
498
|
// Right after a compaction the base context (system prompt, tools, summary) may itself be
|
|
479
499
|
// near a very small window; require real growth so we don't compact on every step.
|
|
@@ -545,6 +565,7 @@ export class Agent {
|
|
|
545
565
|
messages: normalizeMessages(this.messages),
|
|
546
566
|
tools: this.toolSchemas(),
|
|
547
567
|
cacheBucket: this.isSubagent ? 'other' : 'main',
|
|
568
|
+
...(this.effort ? { effort: this.effort } : {}),
|
|
548
569
|
}, { signal, onEvent: (ev) => this.io.streamEvent?.(ev, this) });
|
|
549
570
|
this.lastContextTokens = contextTokens(res.usage);
|
|
550
571
|
this.tokensSinceUsage = 0;
|
|
@@ -618,7 +639,7 @@ export class Agent {
|
|
|
618
639
|
const fail = (content, extra = {}) => ({ block: { type: 'tool_result', tool_use_id: tu.id, is_error: true, content }, ...extra });
|
|
619
640
|
const tool = map.get(tu.name);
|
|
620
641
|
if (!tool) return fail(`Error: No such tool available: ${tu.name}`);
|
|
621
|
-
const input = tu.input || {};
|
|
642
|
+
const input = tool.normalizeInput && tu.input && typeof tu.input === 'object' ? tool.normalizeInput({ ...tu.input }) : (tu.input || {});
|
|
622
643
|
if (input.__invalid_json !== undefined) {
|
|
623
644
|
return fail('Error: the tool input was not valid JSON (it may have been truncated by the output token limit). Try again with a smaller input, e.g. write large files in several steps.');
|
|
624
645
|
}
|
|
@@ -705,7 +726,7 @@ export class Agent {
|
|
|
705
726
|
// the request is too long by the size of the instruction) the plainer request is made: a short system prompt
|
|
706
727
|
// and no effort, which cannot share the cache, but is smaller and cannot be led astray by the main prompt.
|
|
707
728
|
async requestSummary(messages, { signal, onEvent }) {
|
|
708
|
-
const call = (request) => streamMessage(this.config, { model: this.model, tools: this.toolSchemas(), messages, cacheBucket: 'other', ...request }, { signal, onEvent });
|
|
729
|
+
const call = (request) => streamMessage(this.config, { model: this.model, tools: this.toolSchemas(), messages, cacheBucket: 'other', ...(this.effort ? { effort: this.effort } : {}), ...request }, { signal, onEvent });
|
|
709
730
|
try {
|
|
710
731
|
const res = await call({ system: this.currentSystem() });
|
|
711
732
|
this.recordUsage(res, 'summary');
|
package/src/agents.js
CHANGED
|
@@ -6,6 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { CONFIG_DIR } from './config.js';
|
|
7
7
|
|
|
8
8
|
const READ_ONLY_TOOLS = ['Glob', 'Grep', 'Read', 'Bash', 'WebFetch', 'WebSearch', 'BashOutput'];
|
|
9
|
+
// The effort levels a Task call or an agent definition may ask for (as Claude Code's Agent tool takes them).
|
|
10
|
+
export const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
9
11
|
|
|
10
12
|
export const BUILTIN_AGENTS = [
|
|
11
13
|
{
|
|
@@ -83,11 +85,14 @@ export function loadAgents(cwd) {
|
|
|
83
85
|
try {
|
|
84
86
|
const parsed = parseFrontmatter(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
85
87
|
if (!parsed?.meta.name || !parsed.meta.description) continue;
|
|
88
|
+
const window = parseInt(parsed.meta.autoCompactWindow, 10);
|
|
86
89
|
agents.set(parsed.meta.name, {
|
|
87
90
|
name: parsed.meta.name,
|
|
88
91
|
description: parsed.meta.description,
|
|
89
92
|
tools: parsed.meta.tools ? toolList(parsed.meta.tools) : '*',
|
|
90
93
|
model: parsed.meta.model && parsed.meta.model !== 'inherit' ? parsed.meta.model : null,
|
|
94
|
+
effort: EFFORTS.includes(parsed.meta.effort?.toLowerCase()) ? parsed.meta.effort.toLowerCase() : null,
|
|
95
|
+
autoCompactWindow: window > 0 ? window : 0,
|
|
91
96
|
prompt: parsed.body,
|
|
92
97
|
source: path.join(dir, f),
|
|
93
98
|
});
|
package/src/api.js
CHANGED
|
@@ -301,11 +301,12 @@ export function buildParams(config, { model, system, messages, tools, maxTokens,
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
export class APIError extends Error {
|
|
304
|
-
constructor(message, { status, retryable, promptTooLong, retryAfterMs = null, rateLimit = null } = {}) {
|
|
304
|
+
constructor(message, { status, retryable, promptTooLong, retryAfterMs = null, rateLimit = null, overloaded = false } = {}) {
|
|
305
305
|
super(message);
|
|
306
306
|
this.status = status;
|
|
307
307
|
this.retryable = retryable;
|
|
308
308
|
this.promptTooLong = promptTooLong;
|
|
309
|
+
this.overloaded = overloaded; // 529 / overloaded_error: waited out with its own backoff (see planRetry)
|
|
309
310
|
this.retryAfterMs = retryAfterMs; // what the server's Retry-After header asked for
|
|
310
311
|
this.rateLimit = rateLimit; // what the rate limit headers say (see ratelimit.js)
|
|
311
312
|
}
|
|
@@ -325,7 +326,8 @@ function describeError(err, config) {
|
|
|
325
326
|
if (status === 402 && config?.usesGateway) msg = `The gateway account's balance is too low for this request. Top up at ${config.gatewayUrl}/user (or run /login to check the balance). ${msg}`;
|
|
326
327
|
const retryable = !status || status === 408 || status === 409 || status === 429 || status >= 500;
|
|
327
328
|
const header = typeof err?.headers?.get === 'function' ? err.headers.get('retry-after') : err?.headers?.['retry-after'];
|
|
328
|
-
|
|
329
|
+
const overloaded = status === 529 || (err?.error?.error?.type || err?.error?.type) === 'overloaded_error';
|
|
330
|
+
return new APIError(msg, { status, retryable, promptTooLong, retryAfterMs: parseRetryAfter(header), rateLimit: parseRateLimit(err?.headers, timing.now()), overloaded });
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
// Our own aborts are named AbortError and the SDK's is an APIUserAbortError. The message test only
|
|
@@ -365,16 +367,17 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
365
367
|
const params = buildParams(config, request, login);
|
|
366
368
|
let authReloaded = false;
|
|
367
369
|
const refused = new Set(); // what the endpoint refused for this request: remembered once a request got through
|
|
368
|
-
const retryState = { attempt: 0, rateLimitHits: 0, waitedMs: 0 };
|
|
369
|
-
let paused =
|
|
370
|
+
const retryState = { attempt: 0, rateLimitHits: 0, waitedMs: 0, overloaded: 0, overloadedWaitMs: 0 };
|
|
371
|
+
let paused = null; // the plan of the pause in progress (see planRetry)
|
|
370
372
|
for (;;) {
|
|
371
373
|
let emitted = false;
|
|
372
374
|
const startedAt = Date.now(); // the cache TTL counts from the start of the request, not from the end of the reply
|
|
373
375
|
try {
|
|
374
376
|
const stream = await client.messages.create(params, { signal });
|
|
375
377
|
if (paused) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
const overloaded = paused.overloaded;
|
|
379
|
+
paused = null;
|
|
380
|
+
onEvent({ type: 'rate_limit_resume', waitedMs: overloaded ? retryState.overloadedWaitMs : retryState.waitedMs, ...(overloaded ? { overloaded } : {}) });
|
|
378
381
|
}
|
|
379
382
|
const blocks = [];
|
|
380
383
|
const jsonBufs = [];
|
|
@@ -428,7 +431,7 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
428
431
|
if (ev.usage) usage = { ...usage, ...Object.fromEntries(Object.entries(ev.usage).filter(([, v]) => v != null)) };
|
|
429
432
|
break;
|
|
430
433
|
case 'error':
|
|
431
|
-
throw Object.assign(new Error(ev.error?.message || 'stream error'), { status: ev.error?.type
|
|
434
|
+
throw Object.assign(new Error(ev.error?.message || 'stream error'), { status: { rate_limit_error: 429, overloaded_error: 529 }[ev.error?.type] || 500 });
|
|
432
435
|
default:
|
|
433
436
|
break;
|
|
434
437
|
}
|
|
@@ -498,10 +501,13 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
498
501
|
}
|
|
499
502
|
if (emitted) onEvent({ type: 'reset' });
|
|
500
503
|
if (plan.kind === 'rate_limit') {
|
|
501
|
-
paused =
|
|
504
|
+
paused = plan;
|
|
502
505
|
onEvent({
|
|
503
506
|
type: 'rate_limit_wait', error: err, delayMs: plan.delayMs, resumeAt: plan.resumeAt, resetAt: plan.resetAt,
|
|
504
|
-
label: plan.label, polling: plan.polling,
|
|
507
|
+
label: plan.label, polling: plan.polling,
|
|
508
|
+
...(plan.overloaded
|
|
509
|
+
? { overloaded: true, waitedMs: retryState.overloadedWaitMs, maxWaitMs: config.overloadedMaxWait }
|
|
510
|
+
: { waitedMs: retryState.waitedMs, maxWaitMs: config.rateLimitMaxWait }),
|
|
505
511
|
loginExpiresAt: loginExpiryToWarnAbout(config, login),
|
|
506
512
|
});
|
|
507
513
|
await sleepUntil(plan.resumeAt, signal);
|
package/src/config.js
CHANGED
|
@@ -56,7 +56,7 @@ export const KNOWN_KEYS = {
|
|
|
56
56
|
DISABLE_AUTOCOMPACT: 'Set to 1 to disable auto-compaction',
|
|
57
57
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS: 'max_tokens for each model response (default 128000; lowered automatically if the API allows less)',
|
|
58
58
|
MAX_THINKING_TOKENS: 'Thinking budget: 0 disables thinking, N enables with budget N',
|
|
59
|
-
CLAUDE_CODE_PROMPT_CACHE_TTL: 'Prompt cache TTL of the conversation itself: 5m or 1h (default: 1h with the subscription login, as in Claude Code, else 5m; 1h writes cost more, they pay off when you pause for more than 5 minutes). Also the promptCacheTtl setting',
|
|
59
|
+
CLAUDE_CODE_PROMPT_CACHE_TTL: 'Prompt cache TTL of the conversation itself: 5m or 1h (default: 1h with the subscription login, as in Claude Code, and with the gateway, else 5m; 1h writes cost more, they pay off when you pause for more than 5 minutes). Also the promptCacheTtl setting',
|
|
60
60
|
CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL: 'Prompt cache TTL of everything else (sub-agents, compaction, side queries): 5m (default) or 1h. Also the subagentPromptCacheTtl setting',
|
|
61
61
|
ENABLE_PROMPT_CACHING_1H: 'Set to 1 to request the 1-hour TTL for both (the two TTL variables above win over it)',
|
|
62
62
|
FORCE_PROMPT_CACHING_5M: 'Set to 1 to force the 5-minute TTL for both, over every other TTL setting',
|
|
@@ -69,6 +69,9 @@ export const KNOWN_KEYS = {
|
|
|
69
69
|
CLAUDE_PRO_MAX_RETRIES: 'Retries for failed API requests (0 = no retries at all, which also turns off the rate limit wait)',
|
|
70
70
|
CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS: 'Longest total time to wait for a rate limit (e.g. the 5-hour window) to reset before continuing by itself (default 21600000 = 6 hours; 0 = report the error instead of waiting)',
|
|
71
71
|
CLAUDE_PRO_RATE_LIMIT_POLL_MS: 'When a rate limit does not say when it resets, try again at most this far apart (default 300000 = 5 minutes)',
|
|
72
|
+
CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS: 'First wait before retrying a request refused as overloaded (529); doubled at each retry (default 1000)',
|
|
73
|
+
CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS: 'Longest wait between retries of an overloaded (529) request (default 32000)',
|
|
74
|
+
CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS: 'How long to keep retrying an overloaded (529) API once the usual retries are used up, before reporting the error (default: CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS, 6 hours; 0 = give up after the retries)',
|
|
72
75
|
BASH_DEFAULT_TIMEOUT_MS: 'Default timeout for Bash commands (default 600000 = 10 minutes)',
|
|
73
76
|
BASH_MAX_TIMEOUT_MS: 'Maximum timeout the model may request for Bash commands (default 3600000 = 1 hour)',
|
|
74
77
|
BASH_MAX_OUTPUT_LENGTH: 'Max characters of Bash output returned to the model; the full text is saved to a file (default 60000)',
|
|
@@ -254,10 +257,10 @@ export class Config {
|
|
|
254
257
|
|
|
255
258
|
aliasTarget(alias) {
|
|
256
259
|
if (this.usesClaudeCode) {
|
|
257
|
-
return { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-
|
|
260
|
+
return { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' }[alias] || null;
|
|
258
261
|
}
|
|
259
262
|
// The gateway prices models by their undated names.
|
|
260
|
-
if (this.usesGateway) return { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-
|
|
263
|
+
if (this.usesGateway) return { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' }[alias] || null;
|
|
261
264
|
if (this.usesCodex) {
|
|
262
265
|
// No Claude here: the big aliases mean the Codex model, haiku the small one when the Codex install has it.
|
|
263
266
|
if (alias === 'haiku') return codexModels(this.env).models.some((m) => m.slug === 'gpt-6-luna') ? 'gpt-6-luna' : this.codexModelSpec;
|
|
@@ -281,7 +284,7 @@ export class Config {
|
|
|
281
284
|
const inner = this.resolveModel(aliased);
|
|
282
285
|
return { ...inner, contextWindow: oneM ? 1_000_000 : inner.contextWindow, label: `${name}${oneM ? '[1m]' : ''} (${inner.label})` };
|
|
283
286
|
}
|
|
284
|
-
const nativeOneM = this.claudeIds && /^claude-(?:sonnet-5(?:-|$)|opus-(?:5(?:-|$)|4-[78](?:-|$)))/i.test(name);
|
|
287
|
+
const nativeOneM = this.claudeIds && /^claude-(?:(?:sonnet|haiku)-5(?:-|$)|opus-(?:5(?:-|$)|4-[78](?:-|$)))/i.test(name);
|
|
285
288
|
// OpenAI models (the codex provider, gpt-6.1-sol on the gateway): the window the Codex install reports,
|
|
286
289
|
// else the 272K of the current GPT models.
|
|
287
290
|
if (/^gpt-/i.test(name)) {
|
|
@@ -337,7 +340,9 @@ export class Config {
|
|
|
337
340
|
: normalizeTtl(this.env.CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL) || normalizeTtl(this.settings?.subagentPromptCacheTtl);
|
|
338
341
|
if (own) return own;
|
|
339
342
|
if (isOn(this.env.ENABLE_PROMPT_CACHING_1H)) return '1h';
|
|
340
|
-
|
|
343
|
+
// The gateway too: measured on its usage records (533 Opus requests), pauses of 5 to 60 minutes rewrote the whole
|
|
344
|
+
// conversation 9 times at 1.25x, $14.77; writing every turn at 2x instead costs $2.80 more.
|
|
345
|
+
return bucket === 'main' && (this.usesClaudeCode || this.usesGateway) ? '1h' : '5m';
|
|
341
346
|
}
|
|
342
347
|
// DISABLE_PROMPT_CACHING turns it off for everything; the per-model variables only for the model that
|
|
343
348
|
// their alias resolves to (a different version pinned as the model keeps its caching), wherever it runs.
|
|
@@ -354,15 +359,20 @@ export class Config {
|
|
|
354
359
|
get maxRetries() { return int(this.env.CLAUDE_PRO_MAX_RETRIES, 6); }
|
|
355
360
|
get rateLimitMaxWait() { return Math.max(0, int(this.env.CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS, 6 * 3_600_000)); }
|
|
356
361
|
get rateLimitPoll() { return Math.max(1000, int(this.env.CLAUDE_PRO_RATE_LIMIT_POLL_MS, 300_000)); }
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
362
|
+
// An overloaded API (529): the backoff, and how long to keep trying once the retries are used up (see planRetry).
|
|
363
|
+
get overloadedRetryBaseDelay() { return Math.max(100, int(this.env.CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS, 1000)); }
|
|
364
|
+
get overloadedRetryMaxDelay() { return Math.max(this.overloadedRetryBaseDelay, int(this.env.CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS, 32_000)); }
|
|
365
|
+
get overloadedMaxWait() { return Math.max(0, int(this.env.CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS, this.rateLimitMaxWait)); }
|
|
366
|
+
|
|
367
|
+
// Effective window for compaction: the configured auto-compact window capped by the model window. `own`: a
|
|
368
|
+
// sub-agent's autoCompactWindow, which takes the place of the configured one.
|
|
369
|
+
compactWindow(model = this.mainModel, own = 0) {
|
|
370
|
+
const configured = own > 0 ? own : int(this.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, 0);
|
|
361
371
|
return configured > 0 ? Math.min(configured, model.contextWindow) : model.contextWindow;
|
|
362
372
|
}
|
|
363
373
|
get autoCompactEnabled() { return !/^(1|true|yes)$/i.test(this.env.DISABLE_AUTOCOMPACT || ''); }
|
|
364
|
-
autoCompactThreshold(model = this.mainModel) {
|
|
365
|
-
const window = this.compactWindow(model);
|
|
374
|
+
autoCompactThreshold(model = this.mainModel, own = 0) {
|
|
375
|
+
const window = this.compactWindow(model, own);
|
|
366
376
|
const pct = int(this.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, 0);
|
|
367
377
|
if (pct > 0 && pct <= 100) return Math.floor(window * pct / 100);
|
|
368
378
|
// Leave room for the summary itself and the next response (replies rarely come close to the
|
package/src/context.js
CHANGED
|
@@ -43,9 +43,12 @@ export function memoryFiles(cwd) {
|
|
|
43
43
|
}
|
|
44
44
|
for (const d of dirs) {
|
|
45
45
|
if (d === os.homedir()) continue; // home is covered by the user memory
|
|
46
|
+
const before = files.length;
|
|
46
47
|
add(path.join(d, 'CLAUDE.md'), 'project');
|
|
47
48
|
add(path.join(d, '.claude', 'CLAUDE.md'), 'project');
|
|
48
|
-
|
|
49
|
+
// As in Claude Code, AGENTS.md stands in for a missing CLAUDE.md: where both exist they are usually copies of
|
|
50
|
+
// each other (one for Claude, one for Codex), and loading both would send the instructions twice.
|
|
51
|
+
if (files.length === before) add(path.join(d, 'AGENTS.md'), 'project');
|
|
49
52
|
add(path.join(d, 'CLAUDE.local.md'), 'local');
|
|
50
53
|
}
|
|
51
54
|
return files;
|
package/src/hooks.js
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
// "PostToolUse": [...], "UserPromptSubmit": [...], "Stop": [...] }
|
|
5
5
|
// The event payload is passed as JSON on stdin (like Claude Code) and in $CLAUDE_PRO_HOOK_INPUT; $CLAUDE_PROJECT_DIR is set too.
|
|
6
6
|
// Exit code 0: fine (stdout is kept as extra context). Exit code 2: block, stderr goes to the model.
|
|
7
|
-
// Any other non-zero exit code
|
|
7
|
+
// Any other non-zero exit code, a hook that cannot start and one that times out are reported as a warning and
|
|
8
|
+
// let through, unless the hook says "onFailure": "block" (as in Claude Code): then they block like exit code 2.
|
|
9
|
+
// <system-reminder> tags in a hook's output are escaped, so that it cannot pass itself off as the harness.
|
|
8
10
|
|
|
9
11
|
import { spawnShell, killTree, findShell, watchShell } from './utils/shell.js';
|
|
10
12
|
import { stripAnsi } from './ui/ansi.js';
|
|
11
13
|
|
|
12
14
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
13
15
|
|
|
16
|
+
export const escapeReminders = (text) => String(text).replace(/<(\/?\s*system-reminder)/gi, '<$1');
|
|
17
|
+
|
|
14
18
|
function matches(matcher, name) {
|
|
15
19
|
if (!matcher || matcher === '*') return true;
|
|
16
20
|
try { return new RegExp(`^(?:${matcher})$`).test(name || ''); } catch { return matcher === name; }
|
|
@@ -69,11 +73,18 @@ export class HookRunner {
|
|
|
69
73
|
const r = await runCommand(this.shell, h.command, { cwd, payload: full, timeout: h.timeout ? h.timeout * 1000 : DEFAULT_TIMEOUT_MS, signal });
|
|
70
74
|
if (r.code === 2) {
|
|
71
75
|
result.blocked = true;
|
|
72
|
-
result.message = r.err || r.out || `Blocked by ${event} hook: ${h.command}
|
|
76
|
+
result.message = escapeReminders(r.err || r.out || `Blocked by ${event} hook: ${h.command}`);
|
|
73
77
|
return result;
|
|
74
78
|
}
|
|
75
|
-
if (r.code !== 0)
|
|
76
|
-
|
|
79
|
+
if (r.code !== 0) {
|
|
80
|
+
const failure = `${event} hook "${h.command}" exited with ${r.code}${r.err ? `: ${r.err.split('\n')[0]}` : ''}`;
|
|
81
|
+
if (h.onFailure === 'block' && !signal?.aborted) {
|
|
82
|
+
result.blocked = true;
|
|
83
|
+
result.message = escapeReminders(`${failure}\n(The hook failed and is set to block on failure.)`);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
result.warnings.push(failure);
|
|
87
|
+
} else if (r.out) result.context.push(escapeReminders(r.out));
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
90
|
return result;
|
package/src/print.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createRuntime } from './runtime.js';
|
|
5
5
|
import { SessionStore } from './session.js';
|
|
6
|
-
import { describePause, shouldAnnounce } from './ratelimit.js';
|
|
6
|
+
import { describePause, resumeText, shouldAnnounce } from './ratelimit.js';
|
|
7
7
|
import { c } from './ui/ansi.js';
|
|
8
8
|
|
|
9
9
|
export async function runPrint({ prompt, flags, cwd, stdout = process.stdout, stderr = process.stderr }) {
|
|
@@ -21,7 +21,7 @@ export async function runPrint({ prompt, flags, cwd, stdout = process.stdout, st
|
|
|
21
21
|
}
|
|
22
22
|
if (ev.type === 'rate_limit_resume') {
|
|
23
23
|
paused = null;
|
|
24
|
-
err(c.yellow(`${who}
|
|
24
|
+
err(c.yellow(`${who}${resumeText(ev)}`));
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
};
|
package/src/ratelimit.js
CHANGED
|
@@ -161,6 +161,11 @@ function limitMessage(err, rl, state, budget, now, why) {
|
|
|
161
161
|
// that reset, as long as it fits in the budget (rateLimitMaxWait, default 6 hours);
|
|
162
162
|
// - a short announced wait: a few quick retries, counted like any other;
|
|
163
163
|
// - nothing announced: back off 1s, 2s, 4s ... up to rateLimitPoll (5 minutes) between attempts, within the budget.
|
|
164
|
+
//
|
|
165
|
+
// An overloaded API (529, overloaded_error) backs off from CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS (1s) to
|
|
166
|
+
// CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS (32s), as in Claude Code. When the usual retries are used up it does not
|
|
167
|
+
// give up but pauses and tries again at the longest delay, like Claude Code's unattended retry mode, for at most
|
|
168
|
+
// overloadedMaxWait (CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS, by default the rate limit budget) in all.
|
|
164
169
|
export function planRetry(err, state, config, { now = Date.now(), rand = Math.random, allowWait = true } = {}) {
|
|
165
170
|
if (!err.retryable) return { action: 'throw' };
|
|
166
171
|
const maxRetries = config.maxRetries;
|
|
@@ -171,6 +176,22 @@ export function planRetry(err, state, config, { now = Date.now(), rand = Math.ra
|
|
|
171
176
|
return { action: 'wait', kind: 'retry', delayMs, resumeAt: now + delayMs, attempt, maxRetries: max };
|
|
172
177
|
};
|
|
173
178
|
|
|
179
|
+
if ((err.status === 529 || err.overloaded) && maxRetries > 0) {
|
|
180
|
+
// Its own count: a pause starts the quick retries over (see streamMessage), and this one must not.
|
|
181
|
+
const n = state.overloaded = (state.overloaded || 0) + 1;
|
|
182
|
+
const base = config.overloadedRetryBaseDelay ?? 1000;
|
|
183
|
+
const cap = Math.max(base, config.overloadedRetryMaxDelay ?? 32_000);
|
|
184
|
+
const delayMs = Math.max(Math.min(cap, base * 2 ** (n - 1)) + Math.floor(rand() * 500), Math.min(err.retryAfterMs ?? 0, MAX_RETRY_AFTER_MS));
|
|
185
|
+
if (n <= maxRetries) return { action: 'wait', kind: 'retry', delayMs, resumeAt: now + delayMs, attempt: n, maxRetries };
|
|
186
|
+
const allowed = allowWait ? (config.overloadedMaxWait ?? budget) : 0;
|
|
187
|
+
const waited = state.overloadedWaitMs || 0;
|
|
188
|
+
if (waited + delayMs > allowed) {
|
|
189
|
+
return { action: 'throw', message: `${err.message} (the API was still overloaded after ${n - 1} retries${waited ? ` and ${humanDuration(waited)} of waiting` : ''})` };
|
|
190
|
+
}
|
|
191
|
+
state.overloadedWaitMs = waited + delayMs;
|
|
192
|
+
return { action: 'wait', kind: 'rate_limit', overloaded: true, delayMs, resumeAt: now + delayMs, resetAt: null, label: 'API overloaded', polling: true, attempt: n, maxRetries: null };
|
|
193
|
+
}
|
|
194
|
+
|
|
174
195
|
if (err.status === 429 && allowWait && maxRetries > 0 && budget > 0) {
|
|
175
196
|
const known = rl?.waitMs ?? null;
|
|
176
197
|
if (known !== null && known < LONG_WAIT_MS) {
|
|
@@ -223,6 +244,7 @@ export function describePause(ev, now = Date.now()) {
|
|
|
223
244
|
}
|
|
224
245
|
const label = capitalize(ev.label || 'rate limit');
|
|
225
246
|
const left = humanDuration(Math.max(0, ev.resumeAt - now));
|
|
247
|
+
if (ev.overloaded) return `The API is overloaded and kept refusing the request. Trying again at ${formatClock(ev.resumeAt, now)} (in ${left}), and every so often after that, then continuing automatically.`;
|
|
226
248
|
const text = ev.polling
|
|
227
249
|
? `${label} reached and the server gave no reset time. Checking again at ${formatClock(ev.resumeAt, now)} (in ${left}), and every few minutes after that, then continuing automatically.`
|
|
228
250
|
: `${label} reached. Pausing until ${formatClock(ev.resumeAt, now)} (in ${left}), then continuing automatically.`;
|
|
@@ -237,6 +259,8 @@ export function describePause(ev, now = Date.now()) {
|
|
|
237
259
|
// announced reset moves a little between waits: those only continue the pause already announced,
|
|
238
260
|
// unless the reset moved by more than two minutes. Waiting for the login to be renewed is a different
|
|
239
261
|
// kind of wait from waiting for the limit, and always worth saying when it begins.
|
|
262
|
+
export const resumeText = (ev) => (ev?.overloaded ? 'The API is answering again; continuing.' : 'The limit has cleared; continuing.');
|
|
263
|
+
|
|
240
264
|
export function shouldAnnounce(prev, ev) {
|
|
241
265
|
if (!prev) return true;
|
|
242
266
|
if (Boolean(prev.waitingForLogin) !== Boolean(ev.waitingForLogin)) return true;
|
package/src/runtime.js
CHANGED
|
@@ -101,8 +101,10 @@ export async function createRuntime({ cwd, flags = {}, io }) {
|
|
|
101
101
|
return true;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// `model` and `effort`: what the Task call asked for, over the agent definition's own, over the session's.
|
|
105
|
+
runtime.spawnSubagent = async ({ def, prompt, description, ctx, model: modelSpec, effort }) => {
|
|
106
|
+
const spec = modelSpec || def.model;
|
|
107
|
+
const model = spec ? config.resolveModel(spec) : config.subagentModel;
|
|
106
108
|
const subIo = io.subagentIo ? io.subagentIo({ def, description, parentCtx: ctx }) : {};
|
|
107
109
|
// The main agent is depth 0; an agent below the nesting limit may start sub-agents itself.
|
|
108
110
|
const depth = (ctx.depth ?? ctx.agent?.depth ?? 0) + 1;
|
|
@@ -120,6 +122,8 @@ export async function createRuntime({ cwd, flags = {}, io }) {
|
|
|
120
122
|
cwd: ctx.cwd,
|
|
121
123
|
name: def.name,
|
|
122
124
|
hooks,
|
|
125
|
+
effort: effort || def.effort,
|
|
126
|
+
compactWindow: def.autoCompactWindow,
|
|
123
127
|
});
|
|
124
128
|
let result;
|
|
125
129
|
try {
|
package/src/tools/files.js
CHANGED
|
@@ -32,9 +32,9 @@ function readText(file, buf = fs.readFileSync(file)) {
|
|
|
32
32
|
return { text, bom: text !== raw, crlf: crlfCount > 0 && crlfCount >= bareLfCount };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function numberLines(lines, start) {
|
|
35
|
+
export function numberLines(lines, start, maxLineChars = MAX_LINE_CHARS) {
|
|
36
36
|
return lines.map((l, i) => {
|
|
37
|
-
const line = l.length >
|
|
37
|
+
const line = l.length > maxLineChars ? l.slice(0, maxLineChars) + '… [line truncated]' : l;
|
|
38
38
|
return `${String(start + i).padStart(6)}\t${line}`;
|
|
39
39
|
}).join('\n');
|
|
40
40
|
}
|
|
@@ -63,6 +63,7 @@ Usage:
|
|
|
63
63
|
- By default, it reads up to ${MAX_LINES} lines starting from the beginning of the file
|
|
64
64
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
65
65
|
- Any lines longer than ${MAX_LINE_CHARS} characters will be truncated
|
|
66
|
+
- A file whose numbered text is longer than ${MAX_OUTPUT_CHARS} characters is refused (or cut there with offset/limit). When you need a large file whole, in one call, pass allow_large: true: then there is no line, character or line length limit, only the room left in the context, and a read that does not fit is refused. Prefer offset/limit or Grep when part of the file will do.
|
|
66
67
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
67
68
|
- This tool can read images (PNG, JPG, GIF, WEBP) and Jupyter notebooks (.ipynb)
|
|
68
69
|
- This tool can only read files, not directories. To list a directory, use ls via the Bash tool.
|
|
@@ -74,6 +75,7 @@ Usage:
|
|
|
74
75
|
file_path: { type: 'string', description: 'The absolute path to the file to read' },
|
|
75
76
|
offset: { type: 'number', description: 'The line number to start reading from. Only provide if the file is too large to read at once' },
|
|
76
77
|
limit: { type: 'number', description: 'The number of lines to read. Only provide if the file is too large to read at once.' },
|
|
78
|
+
allow_large: { type: 'boolean', description: 'Read past the usual size limits in one call (the whole file, or the whole offset/limit range, with long lines kept whole), when you need all of it and the context has room. Refused if it would not fit in the context left.' },
|
|
77
79
|
},
|
|
78
80
|
required: ['file_path'],
|
|
79
81
|
},
|
|
@@ -125,19 +127,27 @@ Usage:
|
|
|
125
127
|
seen(true);
|
|
126
128
|
return { content: `<system-reminder>Warning: the file exists but is shorter than the provided offset (${start}). The file has ${lines.length} lines.</system-reminder>`, display: { summary: 'Read 0 lines' } };
|
|
127
129
|
}
|
|
128
|
-
|
|
130
|
+
// allow_large: no line, character or line length limit, only the room left in the context (see Agent.takeContext).
|
|
131
|
+
const large = input.allow_large === true;
|
|
132
|
+
const limit = Math.max(1, Math.floor(input.limit || (large ? Infinity : MAX_LINES)));
|
|
129
133
|
const slice = lines.slice(start - 1, start - 1 + limit);
|
|
130
|
-
let content = numberLines(slice, start);
|
|
131
|
-
if (
|
|
132
|
-
|
|
134
|
+
let content = numberLines(slice, start, large ? Infinity : MAX_LINE_CHARS);
|
|
135
|
+
if (large) {
|
|
136
|
+
const fit = ctx.agent?.takeContext?.(content);
|
|
137
|
+
if (fit && !fit.ok) {
|
|
138
|
+
const n = (t) => t.toLocaleString('en-US');
|
|
139
|
+
return { isError: true, content: `File content (about ${n(fit.tokens)} tokens) does not fit in the context left (about ${n(Math.max(0, fit.room))} tokens before auto-compaction). Read the parts you need with offset and limit, or use the Grep tool to search for specific content.` };
|
|
140
|
+
}
|
|
141
|
+
} else if (content.length > MAX_OUTPUT_CHARS && !input.limit) {
|
|
142
|
+
return { isError: true, content: `File content (${Math.round(content.length / 1024)}KB) exceeds the maximum allowed size. Use the offset and limit parameters to read specific portions of the file, or use the Grep tool to search for specific content. If you need the whole file at once, pass allow_large: true.` };
|
|
133
143
|
}
|
|
134
144
|
let cut = false;
|
|
135
145
|
let end = start + slice.length - 1;
|
|
136
|
-
if (content.length > HARD_MAX_CHARS) {
|
|
146
|
+
if (!large && content.length > HARD_MAX_CHARS) {
|
|
137
147
|
const kept = content.slice(0, HARD_MAX_CHARS);
|
|
138
148
|
const whole = kept.endsWith('\n'); // the cut fell between two lines
|
|
139
149
|
end = start + (kept.match(/\n/g) || []).length - (whole ? 1 : 0); // the last line shown
|
|
140
|
-
content = `${kept}${whole ? '' : '\n'}… [output cut at ${HARD_MAX_CHARS} characters: line ${end} is the last one shown${whole ? '' : ', and only in part'}; read on from there with offset/limit]`;
|
|
150
|
+
content = `${kept}${whole ? '' : '\n'}… [output cut at ${HARD_MAX_CHARS} characters: line ${end} is the last one shown${whole ? '' : ', and only in part'}; read on from there with offset/limit, or pass allow_large: true to read it all at once]`;
|
|
141
151
|
cut = true;
|
|
142
152
|
}
|
|
143
153
|
seen(cut || start > 1 || end < lines.length);
|
package/src/tools/search.js
CHANGED
|
@@ -385,6 +385,14 @@ Usage:
|
|
|
385
385
|
},
|
|
386
386
|
required: ['pattern'],
|
|
387
387
|
},
|
|
388
|
+
// What a model used to grep (or to the Read tool) sometimes sends, taken as meant, as Claude Code does, instead of
|
|
389
|
+
// searching the working directory when it named a file_path: -l and -c pick the output mode, -r is the default.
|
|
390
|
+
normalizeInput(input) {
|
|
391
|
+
if (input.path == null && typeof input.file_path === 'string') input.path = input.file_path;
|
|
392
|
+
if (input.output_mode == null && input['-c']) input.output_mode = 'count';
|
|
393
|
+
else if (input.output_mode == null && input['-l']) input.output_mode = 'files_with_matches';
|
|
394
|
+
return input;
|
|
395
|
+
},
|
|
388
396
|
summarize(input, ctx) {
|
|
389
397
|
let s = `pattern: "${input.pattern}"`;
|
|
390
398
|
if (input.path) s += `, path: "${displayPath(resolvePath(input.path, ctx.cwd), ctx.cwd)}"`;
|
package/src/tools/task.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// background tasks.
|
|
4
4
|
|
|
5
5
|
import { taskResultText } from '../tasks.js';
|
|
6
|
+
import { EFFORTS } from '../agents.js';
|
|
6
7
|
|
|
7
8
|
// Wait for `promise`, at most `ms`, or until `signal` aborts. The timer is always cleared so a
|
|
8
9
|
// finished wait never keeps the process alive.
|
|
@@ -52,16 +53,24 @@ Usage notes:
|
|
|
52
53
|
prompt: { type: 'string', description: 'The task for the agent to perform' },
|
|
53
54
|
subagent_type: { type: 'string', description: 'The type of specialized agent to use for this task' },
|
|
54
55
|
run_in_background: { type: 'boolean', description: 'Run the agent in the background and return immediately with a task id' },
|
|
56
|
+
model: { type: 'string', description: 'Optional model for this agent: sonnet, opus, haiku or a full model id. Defaults to the agent definition\'s model, else the sub-agent default. A smaller model is cheaper and faster for simple searches.' },
|
|
57
|
+
effort: { type: 'string', enum: EFFORTS, description: 'Optional effort level for this agent (default: the agent definition\'s, else the session\'s). Low suits quick lookups, high or above hard reasoning.' },
|
|
55
58
|
},
|
|
56
59
|
required: ['description', 'prompt', 'subagent_type'],
|
|
57
60
|
},
|
|
58
|
-
summarize(input) {
|
|
61
|
+
summarize(input) {
|
|
62
|
+
const on = [input.model, input.effort].filter(Boolean).join(' · ');
|
|
63
|
+
return `${input.description || ''}${on ? ` (${on})` : ''}${input.run_in_background ? ' (background)' : ''}`;
|
|
64
|
+
},
|
|
59
65
|
label(input) { return input.subagent_type && input.subagent_type !== 'general-purpose' ? input.subagent_type : 'Task'; },
|
|
60
66
|
async call(input, ctx) {
|
|
61
67
|
const def = agents.get(input.subagent_type) || agents.get('general-purpose');
|
|
62
68
|
if (!agents.has(input.subagent_type) && input.subagent_type) {
|
|
63
69
|
return { isError: true, content: `Agent type '${input.subagent_type}' not found. Available agents: ${[...agents.keys()].join(', ')}` };
|
|
64
70
|
}
|
|
71
|
+
const effort = input.effort ? String(input.effort).toLowerCase() : undefined;
|
|
72
|
+
if (effort && !EFFORTS.includes(effort)) return { isError: true, content: `Unknown effort '${input.effort}'. Use one of: ${EFFORTS.join(', ')}.` };
|
|
73
|
+
const choice = { model: input.model?.trim() || undefined, effort };
|
|
65
74
|
if (input.run_in_background) {
|
|
66
75
|
if (!ctx.tasks) return { isError: true, content: 'Background agents are not available here; run the task in the foreground.' };
|
|
67
76
|
// The task outlives this turn, so it gets its own signal and no live progress display.
|
|
@@ -69,7 +78,7 @@ Usage notes:
|
|
|
69
78
|
const task = ctx.tasks.start({
|
|
70
79
|
description: input.description || def.name,
|
|
71
80
|
subagentType: def.name,
|
|
72
|
-
run: (signal) => ctx.spawnSubagent({ def, prompt: input.prompt, description: input.description, ctx: { ...base, signal } }),
|
|
81
|
+
run: (signal) => ctx.spawnSubagent({ def, prompt: input.prompt, description: input.description, ctx: { ...base, signal }, ...choice }),
|
|
73
82
|
});
|
|
74
83
|
return {
|
|
75
84
|
content: `Started background agent ${task.id} (${def.name}): ${input.description}. Its final report will arrive in a <task-notification> when it finishes. Continue with other work; use TaskOutput with task_id "${task.id}" only if you need to wait for it.`,
|
|
@@ -77,7 +86,7 @@ Usage notes:
|
|
|
77
86
|
};
|
|
78
87
|
}
|
|
79
88
|
const started = Date.now();
|
|
80
|
-
const { text, toolUses, tokens } = await ctx.spawnSubagent({ def, prompt: input.prompt, description: input.description, ctx });
|
|
89
|
+
const { text, toolUses, tokens } = await ctx.spawnSubagent({ def, prompt: input.prompt, description: input.description, ctx, ...choice });
|
|
81
90
|
const secs = Math.round((Date.now() - started) / 100) / 10;
|
|
82
91
|
const kTok = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
83
92
|
return {
|
package/src/ui/editor.js
CHANGED
|
@@ -99,12 +99,32 @@ export class Editor {
|
|
|
99
99
|
|
|
100
100
|
clear() {
|
|
101
101
|
this.set('');
|
|
102
|
-
this.pastes
|
|
103
|
-
this.images
|
|
102
|
+
this.pastes = new Map();
|
|
103
|
+
this.images = new Map();
|
|
104
104
|
this.imageCounter = 0;
|
|
105
105
|
this.historyIndex = null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Clear a draft the user threw away (ctrl+c, esc esc), as Claude Code does: up on the empty prompt brings it back,
|
|
109
|
+
// with its pasted text and images, even after other messages were sent meanwhile.
|
|
110
|
+
discard() {
|
|
111
|
+
if (this.text.trim()) {
|
|
112
|
+
this.cleared = { text: this.text, cursor: this.cursor, pastes: this.pastes, images: this.images, imageCounter: this.imageCounter };
|
|
113
|
+
}
|
|
114
|
+
this.clear();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
restoreCleared() {
|
|
118
|
+
const d = this.cleared;
|
|
119
|
+
if (!d || this.text || this.historyIndex !== null) return false;
|
|
120
|
+
this.cleared = null;
|
|
121
|
+
this.pastes = d.pastes;
|
|
122
|
+
this.images = d.images;
|
|
123
|
+
this.imageCounter = d.imageCounter;
|
|
124
|
+
this.set(d.text, d.cursor);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
insertImage(image) {
|
|
109
129
|
const label = `[Image #${++this.imageCounter}]`;
|
|
110
130
|
this.images.set(label, image);
|
|
@@ -213,9 +233,9 @@ export class Editor {
|
|
|
213
233
|
}
|
|
214
234
|
if (name === 'escape') {
|
|
215
235
|
if (this.suggestions) { this.suggestionsDismissedFor = this.text; this.suggestions = null; return true; }
|
|
216
|
-
if (key.meta && this.text) { this.
|
|
236
|
+
if (key.meta && this.text) { this.discard(); return true; } // ESC ESC arriving in one chunk
|
|
217
237
|
const now = Date.now();
|
|
218
|
-
if (this.text && now - this.lastEscape < 800) { this.
|
|
238
|
+
if (this.text && now - this.lastEscape < 800) { this.discard(); this.lastEscape = 0; return true; }
|
|
219
239
|
this.lastEscape = now;
|
|
220
240
|
return false;
|
|
221
241
|
}
|
|
@@ -228,7 +248,7 @@ export class Editor {
|
|
|
228
248
|
this.set(this.text, Math.min(prevStart + col, lineStart - 1));
|
|
229
249
|
return true;
|
|
230
250
|
}
|
|
231
|
-
return this.historyPrev();
|
|
251
|
+
return this.restoreCleared() || this.historyPrev();
|
|
232
252
|
}
|
|
233
253
|
if (name === 'down' || (key.ctrl && name === 'n')) {
|
|
234
254
|
if (this.suggestions) { this.selected = (this.selected + 1) % this.suggestions.items.length; this.onChange?.(); return true; }
|
package/src/ui/repl.js
CHANGED
|
@@ -24,7 +24,7 @@ import { buildCommands } from '../commands.js';
|
|
|
24
24
|
import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
|
|
25
25
|
import { resolvePath, displayPath } from '../utils/paths.js';
|
|
26
26
|
import { ripgrepPath } from '../utils/shell.js';
|
|
27
|
-
import { describePause, formatClock, humanDuration, shouldAnnounce } from '../ratelimit.js';
|
|
27
|
+
import { describePause, formatClock, humanDuration, resumeText, shouldAnnounce } from '../ratelimit.js';
|
|
28
28
|
import { shutdownBackgroundShells, listBackgroundShells } from '../tools/bash.js';
|
|
29
29
|
import { estimateTokens } from '../agent.js';
|
|
30
30
|
import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
|
|
@@ -625,7 +625,7 @@ export class App {
|
|
|
625
625
|
return;
|
|
626
626
|
}
|
|
627
627
|
if (this.busy) { this.interrupt(); return; }
|
|
628
|
-
if (this.editor.text) { this.editor.
|
|
628
|
+
if (this.editor.text) { this.editor.discard(); this.screen.schedule(); return; }
|
|
629
629
|
if (Date.now() - this.exitArmed < 1500) { this.exit(0); return; }
|
|
630
630
|
this.exitArmed = Date.now();
|
|
631
631
|
this.screen.schedule();
|
|
@@ -1149,12 +1149,12 @@ export class App {
|
|
|
1149
1149
|
rateLimitEvent(ev) {
|
|
1150
1150
|
if (ev.type === 'rate_limit_resume') {
|
|
1151
1151
|
this.rateWait = null;
|
|
1152
|
-
this.notice(
|
|
1152
|
+
this.notice(resumeText(ev));
|
|
1153
1153
|
return;
|
|
1154
1154
|
}
|
|
1155
1155
|
// Say it once; later waits of the same pause only move the countdown (unless the reset moved a lot).
|
|
1156
1156
|
const prev = this.rateWait;
|
|
1157
|
-
this.rateWait = { resumeAt: ev.resumeAt, label: ev.label, polling: ev.polling, waitingForLogin: Boolean(ev.waitingForLogin) };
|
|
1157
|
+
this.rateWait = { resumeAt: ev.resumeAt, label: ev.label, polling: ev.polling, waitingForLogin: Boolean(ev.waitingForLogin), overloaded: Boolean(ev.overloaded) };
|
|
1158
1158
|
if (shouldAnnounce(prev, ev)) this.notice(`${describePause(ev)} Esc cancels.`, 'warn');
|
|
1159
1159
|
this.screen.schedule();
|
|
1160
1160
|
}
|
|
@@ -1238,7 +1238,7 @@ export class App {
|
|
|
1238
1238
|
}
|
|
1239
1239
|
if (ev.type === 'rate_limit_resume') {
|
|
1240
1240
|
paused = null;
|
|
1241
|
-
app.notice(`[${def.name}]
|
|
1241
|
+
app.notice(`[${def.name}] ${resumeText(ev)}`);
|
|
1242
1242
|
}
|
|
1243
1243
|
};
|
|
1244
1244
|
return {
|
|
@@ -1321,7 +1321,7 @@ export class App {
|
|
|
1321
1321
|
if (w.waitingForLogin) return c.yellow(`${frame} Waiting for the Claude Code login to be renewed · checking again in ${left}`) + ' ' + esc;
|
|
1322
1322
|
const label = w.label.charAt(0).toUpperCase() + w.label.slice(1);
|
|
1323
1323
|
const when = w.polling ? `checking again in ${left}` : `resumes at ${formatClock(w.resumeAt)} (in ${left})`;
|
|
1324
|
-
return c.yellow(`${frame} ${label} reached · ${when}`) + ' ' + esc;
|
|
1324
|
+
return c.yellow(`${frame} ${w.overloaded ? label : `${label} reached`} · ${when}`) + ' ' + esc;
|
|
1325
1325
|
}
|
|
1326
1326
|
const secs = formatDuration(Date.now() - this.turnStart);
|
|
1327
1327
|
const verb = this.status || (this.thinking ? 'Thinking' : this.verb);
|