saaranshM/unsnooze
GitHub: saaranshM/unsnooze
unsnooze 是一款自动监控并恢复因使用配额限制而中断的 AI CLI Agent 会话的开发者工具,支持多种主流 AI 编程助手和多路复用器。
Stars: 22 | Forks: 2
[](https://github.com/saaranshM/unsnooze/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/unsnooze)
[](package.json)
[](LICENSE)
**Claude Code · Codex CLI · Grok · Qwen · Kimi · OpenCode · Antigravity** — 当它们触及 5 小时或每周的使用限制时
("You've hit your usage limit"),你的会话就会……直接停止。
unsnooze 会自动恢复它们:它会在你所有的项目中追踪**每一个**因触及限制而停止的会话,并且**在限制重置的瞬间,于 tmux 或 Zellij 中唤醒每一个会话。**
```
npm install -g unsnooze && unsnooze setup
```
## 为什么选择 unsnooze
通宵及长时间运行的 agent 任务常常会在触及 5 小时/每周限制时中断,而现有的每种工具只解决了其中一小部分问题:
| | **unsnooze** | claude-auto-retry | autoclaude | hydra |
|---|:---:|:---:|:---:|:---:|
| 多 CLI(Claude · Codex · Grok · Qwen · Kimi · OpenCode · Antigravity) | ✅ | ❌ 仅限 Claude | ❌ 仅限 Claude | 部分 |
| GUI 会话(VS Code 扩展,桌面应用) | ✅ watcher 守护进程 | ❌ | ❌ | ❌ |
| 等待重置并恢复**同一个**会话 | ✅ | ✅ | ✅ | ❌ 切换提供商 |
| 同时处理所有会话(共享账本 + 单个守护进程) | ✅ | ❌ 单个窗格 | ✅ | ✅ |
| 恢复其窗格/进程已**消失**的会话 | ✅ `--resume
` | ❌ | ❌ | ❌ |
| 能够在笔记本休眠及长达一周的等待中存活 | ✅ epoch 轮询 | 部分 | 部分 | 不适用 |
| 设置 + 首次运行向导 | ✅ | ❌ | ❌ | ❌ |
## 支持的 CLI
- **Claude Code** — 双通道检测:`StopFailure` hook(权威来源,携带 `session_id`)结合多路复用器窗格抓取,用于获取横幅和交互式限制菜单(总是回答 *"Stop and wait for limit to reset"*,绝不盲按 Enter)。死去的会话通过 `claude --resume ` 恢复。
- **OpenAI Codex CLI** — 基于抓取(Codex 在触发限制时不发送任何事件)。检测来自 Codex 源码的确切 `■ You've hit your usage limit …` 横幅字符串,解析 `try again at 3:51 PM` / `Feb 23rd, 2026 9:01 PM` / `in 4 days 20 hours 9 minutes`。死去的会话通过 `codex resume ""` 恢复 — prompt 通过 argv 传递。
- **Grok Build (xAI)** — ⚠️ *实验性*。Hook 通道工作正常(Grok 读取兼容 Claude 的 hook,包括 `StopFailure`);限制横幅文本未公开记录,因此检测使用带有安全回退的通用模式。遇到 unsnooze 遗漏的横幅了?运行 `unsnooze report` 并将捕获的内容粘贴到 issue 中 — 这就是让这个适配器变得更好的方法。
- **Qwen Code** — ⚠️ *实验性*。双通道:安装到 `~/.qwen/settings.json` 的类 Claude 的 `StopFailure` hook(伴随 `error: rate_limit` 触发),结合对原文配额渲染的抓取(`Qwen OAuth quota exceeded`,Coding Plan `Allocated quota exceeded`,以及 OpenRouter `Rate limit exceeded: limit_…` 透传)。Qwen 从不显示重置时间,因此等待使用 5 小时回退并在验证时自我纠正。死去的会话通过 `qwen --resume ` 恢复(会话 ID 来自 qwen 专门为此目的编写的 `*.runtime.json` sidecar 文件)。
- **Kimi CLI (Moonshot)** — ⚠️ *实验性*。基于抓取:kimi 会在几秒钟内重试 429 三次,然后停止并显示红色的 `LLM provider error: Error code: 429 … rate_limit_reached_error` 行 — 这就是检测锚点。429 响应体不包含重置时间(5 小时回退 + 验证)。死去的会话通过 `kimi -r -p ""` 恢复 — 带有防护措施:kimi 会对未知 id 静默启动一个*新*会话,因此首先会在磁盘上验证 id(否则使用 `--continue`)。`Membership expired` (402) 仅用于通知。
- **OpenCode** — ⚠️ *实验性*,且形态不同:OpenCode *自身会永远重试速率限制*,遵循 `retry-after`(它会休眠数小时直到重置,显示 `Rate Limited [retrying in 2h5m attempt #4]`)。因此 unsnooze 会记录停止状态,但绝不会触碰正在实时自我重试的窗格;它的任务是恢复那些在等待期间进程死掉的会话(笔记本休眠,tmux 消失),通过 `opencode -s `,并直接从横幅倒计时解析出重置时间。Zen 计划横幅(`5 hour/weekly/monthly usage limit reached…`)和 OpenRouter 透传也会被检测到;`insufficient credits` (402) 仅用于通知。
- **Antigravity CLI (Google, `agy`)** — ⚠️ *实验性*。Gemini-CLI 的继任者。抓取论坛报告的配额字符串(`Model quota limit exceeded`,`Refreshes in 6 days and 18 hours` — 已解析,多天刷新 = 每周上限),并将 `503 MODEL_CAPACITY_EXHAUSTED` 视为瞬时过载,而非限制。死去的会话通过 `agy --conversation=` 恢复(id 来自 `~/.gemini/antigravity-cli/history.jsonl`)。与 Grok 一样:闭源,因此 `unsnooze report` 的捕获能让这个适配器变得更好。
**OpenRouter**(API 网关)不是一个单独的 agent:其 429 响应体(`Rate limit exceeded: limit_rpd/…`,免费每日模型限制)会在使用它的 CLI(OpenCode、Qwen Code)内部被检测到,而额度耗尽 (402) 会作为通知显示 — 没有可等待的重置,只能充值。
## GUI 界面(VS Code 扩展,桌面应用)
终端会话通过 shell 包装器 + tmux 或 Zellij 进行监视。**Claude Code 的 VS Code 扩展 / 桌面应用**和 **Codex 的 IDE 扩展 / 桌面应用**中的会话没有窗格可供抓取 — 因此 `unsnooze daemon` 会追踪这些界面已经写入的会话文件:
- **Claude Code** 将每次速率限制停止记录为其 `~/.claude/projects/**.jsonl` 转录文件中的结构化条目(会话 id、cwd、重置时间) — 由 CLI 和 VS Code 扩展共享。
- **Codex** 将 `rate_limits` 快照(使用率 %,**确切的 epoch 重置时间**)写入 `~/.codex/sessions/` 下的每一次 rollout 中 — 由 CLI、IDE 扩展和**统一的 ChatGPT 桌面应用**共享(2026 年 7 月:Codex 应用成为 ChatGPT 应用;其捆绑的 `codex app-server` 将相同的 rollout 写入相同的存储中 — 已针对真实安装进行了验证)。在 Codex 仅存在于 ChatGPT.app 内部(PATH 中没有 `codex`)的机器上,unsnooze 会自动通过应用捆绑的二进制文件(`/Applications/ChatGPT.app/Contents/Resources/codex`)恢复。
- **Claude 桌面版(协作)会话** *(实验性,macOS)* 运行在 `~/Library/Application Support/Claude` 下的沙箱中;unsnooze 也会监视这些会话,并使用会话隔离的 `CLAUDE_CONFIG_DIR` 进行恢复(加上保持认证正常工作的 keychain 范围覆盖 — 已针对真实的桌面会话进行了验证)。
当限制重置时,会话将通过 `claude --resume ` / `codex resume ` **在多路复用器窗格中**恢复 — 它是同一个会话文件,因此持续的对话在 GUI 自身的历史记录中仍然可见。(目前无法*在* GUI 面板内恢复:没有任何扩展/应用暴露可以发送 prompt 的 IPC/URI。)
在 `unsnooze setup` 中启用它(安装 launchd agent / systemd user unit),或者自己运行 `unsnooze install --daemon` / `unsnooze daemon`。随时可以通过 `unsnooze config set guiWatch off` 关闭它。
## 工作原理
文本版本
```
claude / codex / grok (shell wrapper) ──► unsnooze _run ──► CLI in mux pane
│
├─ per-pane monitor (scrapes for limit
│ banners, drives Claude's limit menu,
│ seconds-scale retry on 5xx/overload)
│
StopFailure hook (claude, grok) ──────────────┤
▼
~/.unsnooze/state.json
{ agent, sessionId, cwd, pane, resetAt, status }
│
▼
resumer daemon (singleton, epoch-polling —
survives laptop sleep and weekly-scale waits)
│
┌───────────────────┴────────────────────┐
pane still alive? pane gone?
send resume message into it new multiplexer pane,
(only if the CLI is foreground `unsnooze _run
and not mid-stream) --resume `, verify
```
限制事件从不会被 CLI 自身持久化;重置时间从横幅文本中解析,支持 DST 安全,并在无法解析时使用 5 小时回退 — 并且每次恢复后都会进行验证(横幅再次出现 → 根据新的横幅重新调度,最多重试 5 次)。
## 用法
```
claude / codex / grok # normal usage — wrapped automatically
unsnooze status # tracked sessions + reset countdowns
unsnooze resume-now [id|--all] # don't wait for the reset time
unsnooze cancel [id|--all] # stop tracking a session
unsnooze message "text" # per-session wake message (--clear to reset)
unsnooze config list # settings (see below)
unsnooze config set # e.g. autoResume off
unsnooze logs [-f] # what unsnooze has been doing
unsnooze update # update unsnooze itself
unsnooze daemon # persistent GUI-session watcher (usually run
# by launchd/systemd via `install --daemon`)
unsnooze report [agent] # capture a pane to report an undetected banner
unsnooze uninstall [--purge] # remove wrappers + hooks (+ state with --purge)
unsnooze help # full command list (also -h / --help)
```
## 设置
`unsnooze setup` 会写入 `~/.unsnooze/config.json`;之后你可以使用 `unsnooze config set` 更改任何内容:
| 键 | 默认值 | 含义 |
|---|---|---|
| `multiplexer` | `auto` | 使用的后端:`auto`、`tmux` 或 `zellij`。`auto` 优先使用当前的多路复用器,其次是唯一安装的后端,并以 tmux 作为决胜条件。 |
| `autoResume` | `true` | 总开关。关闭 = 仍然追踪停止状态,但在你运行 `unsnooze resume-now` 或将其重新打开之前,不会恢复任何内容。 |
| `menuAutoAnswer` | `true` | 是否允许 unsnooze 回答 Claude 的限制菜单(在你的窗格中发送按键)?关闭 = 仅监视。 |
| `notifications` | `true` | 所有通知的总开关(检测到限制 / 会话已恢复 / 放弃)。关闭 = 静音所有通道。 |
| `notifyChannel` | `auto` | 交付方式:`auto`、`native`、`osc` 或 `bell`(参见[通知通道](#notification-channels))。环境变量:`UNSNOOZE_NOTIFY_CHANNEL`。 |
| `guiWatch` | `true` | 是否允许守护进程监视 GUI 界面停止(VS Code 扩展,桌面应用)的会话文件?需要守护进程运行(`unsnooze install --daemon`)。 |
| `resumeMessage` | *"Continue where you left off…"* | 发送以唤醒会话的消息。针对单个会话覆盖它,请使用 `unsnooze message "…"` — 在 `unsnooze status` 中可见。 |
| `resumeMessages.claude` / `.codex` / `.grok` / `.qwen` / `.kimi` / `.opencode` / `.agy` | `""` | 针对 agent 覆盖 `resumeMessage`。空 = 使用全局消息;使用 `unsnooze config set resumeMessages.claude ""` 清除。 |
| `agents.claude` / `agents.codex` | `true` | 受保护的 CLI。 |
| `agents.grok` / `agents.qwen` / `agents.kimi` / `agents.opencode` / `agents.agy` | `false` | 实验性适配器 — 默认关闭;在 `unsnooze setup` 或 `unsnooze config set agents.qwen on` 中启用。 |
| `workspaceGuard` | `inform` | 会话休眠期间仓库发生了变化?`inform` 在消息中附带提醒来唤醒它;`pause` 暂停它(桌面通知,差异在 `resume-now` 时显示);`off` 禁用。 |
| `contextGuard` | `inform` | 唤醒时出现巨大的冷上下文?唤醒会话意味着它需要**以完全未缓存的全价重新读取其整个上下文**([原因](#why-did-resuming-a-big-session-eat-so-much-of-my-quota))。`inform` 恢复并通知你大小;`pause` 将超过阈值的会话挂起,等待 `unsnooze resume-now`;`off` 禁用。目前仅适用于 Claude Code |
| `contextGuardTokens` | `100000` | 上下文大小阈值(以 token 为单位),达到该阈值时 `contextGuard` 会通知或暂停。 |
| `updateCheck` | `true` | 每日新版本检查(对 npm 注册表进行简单的 GET 请求,不发送任何身份识别信息)。在命令执行后发出通知 + 每个版本一个桌面 toast。 |
每个设置都有一个 `UNSNOOZE_*` 环境变量覆盖(参见 `src/settings.js`),并且所有时间/路径都可以通过 `UNSNOOZE_*` 环境变量进行调节(参见 `src/config.js`)。
### 通知通道
当触及限制、会话恢复或 unsnooze 放弃时,它可以通过 OS toast、终端 OSC 序列、BEL 或它们的组合来提醒你 — 由 `notifyChannel`(默认为 `auto`)控制。`notifications=false`(或 `UNSNOOZE_NOTIFICATIONS=off`)会关闭所有通道。
| 通道 | 作用 |
|---|---|
| `auto` | OSC(当终端支持时)**加上**在窗格 tty 上发送 BEL;仅在 OSC 未交付任何内容时回退到 native(避免重复横幅)。无窗格 / 非 tmux 多路复用器 → native。 |
| `native` | OS toast(macOS `osascript`,Linux `notify-send`,WSL/Windows PowerShell toast);在其他平台上,tmux 的 `display-message` 作为最后手段的状态栏回退。 |
| `osc` | 强制向已连接的客户端 tty 发送 OSC;如果交付数量为零则使用 native。 |
| `bell` | 向窗格 tty 发送 BEL;如果无法交付则使用 native。 |
**终端支持 (OSC)** — 检测使用 `TERM_PROGRAM` / termname / 已知的环境标记:
| 终端 | 序列 |
|---|---|
| iTerm2, kitty, WezTerm, Ghostty, Warp | OSC 9 (`\x1b]9;title: body\x07`) |
| rxvt / urxvt | OSC 777 (`\x1b]777;notify;title;body\x07`) |
| Apple Terminal, VS Code, Alacritty, Zed | 黑名单 — 跳过 OSC(首选 `native`;强制 `osc` 并不能解锁这些) |
| 未知 | 在 `auto` 模式下跳过;当 `notifyChannel=osc` 时使用 OSC 9 |
**OSC/BEL 仅适用于 tmux。** 这些路径会写入客户端的 tty (OSC) 或窗格的 tty (BEL),这需要 tmux 的客户端/窗格 tty API。**Zellij 没有等效功能**,因此不会在 Zellij 下尝试 OSC/BEL — 通知回退到 native(而且 Zellij 也没有等效的状态栏注入功能)。GUI 监视器停止(无窗格上下文)总是使用 native。
## 安全特性
- **绝不盲目注入**:仅当窗格的前台进程是 agent CLI 并且没有可见类似 "esc to interrupt" 的繁忙页脚时,才会发送按键。回收的窗格 id 无法接收到杂乱的消息。
- **绝不盲目选择菜单选项**:如果无法读取 Claude 的限制菜单布局,unsnooze 将不会按下 Enter(那可能会确认 "Upgrade your plan")。
- **休眠安全的等待**:恢复程序每 30 秒轮询一次挂钟时间与目标 epoch 的对比,而不是使用一个长时间计时器 — 休眠超过重置时间的笔记本会在下一个节拍触发。每周限制只是一个更大的 epoch。
- **经过验证的恢复**:在调度后,它会重新捕获窗格;如果限制横幅再次出现(重置时间解析错误 / 实际未重置),它会根据新的横幅重新调度,最多重试 5 次。
- **并发写入安全**:状态更新通过 mkdir 锁 + 原子重命名进行;损坏的状态会被隔离,从不导致致命错误(hook 路径绝不能阻塞 CLI)。
- **过载 ≠ 限制**:5xx/529/429 瞬时错误采取秒级的退避路径([30,60,120,240,300]s ± 抖动),并且从不进入账本。
- **陈旧工作区防护**:在会话停止时对仓库的 HEAD + 脏状态进行指纹识别。如果另一个会话(或你)在唤醒之前更改了仓库,恢复的 agent 会被要求在行动前重新阅读 — 或者,在 `workspaceGuard=pause` 的情况下,会话会被挂起,并且 `unsnooze resume-now` 会首先显示差异。
- **冷缓存唤醒防护**:在数小时后唤醒会话意味着提供商的 prompt 缓存早已过期 — 第一条消息会以全价重新读取整个上下文,而一个庞大的会话可能会消耗掉新配额窗口的很大一部分。unsnooze 根据会话转录估计大小(在 `unsnooze status` 中显示为 `ctx ~152k tok`),并根据 `contextGuard`,通知你或挂起会话,而不是在后台默默消耗配额。
## 环境要求
- Node ≥ 20 和 tmux ≥ 3.2 **或者** Zellij
- macOS、Linux 或 **通过 WSL 使用 Windows**(见下文)
- zsh 或 bash(包装器安装到 `~/.zshrc` / `~/.bashrc`)
### Windows / WSL
tmux 和 Zellij 是 Unix 工具,因此在 Windows 上,unsnooze 运行在 [WSL](https://learn.microsoft.com/windows/wsl/install) 内部 — 这也是 agent CLI 在 Windows 上的运行环境:
```
# 在你的 WSL distro 内(Ubuntu 等)
sudo apt install tmux # or install Zellij: https://zellij.dev/documentation/installation
npm install -g unsnooze && unsnooze setup
```
在 macOS 上,使用 `brew install tmux` 或 `brew install zellij` 安装任一后端。在 `auto` 模式下,unsnooze 使用你当前所在的多路复用器;可以通过 `unsnooze config set multiplexer tmux` 或 `zellij` 明确选择一个。
一切都像在 Linux 上一样工作,包括桌面通知:在 WSL 内部,unsnooze 会通过 `powershell.exe` 弹出**原生 Windows toast**(不需要 `notify-send` 或 X 服务器)。不支持原生 Windows(PowerShell/cmd,无 WSL) — 没有 tmux 或 Zellij,就没有可监视的终端窗格;unsnooze 会告诉你这一点,并在不破坏你 CLI 的情况下以无监视模式运行它。
## 常见问题
### "You've hit your usage limit" 在 Claude Code 中是什么意思?
Claude 订阅计划在滚动 5 小时窗口加上每周上限内计量使用量。当其中任何一个耗尽时,Claude Code 会在任务执行中途停止,并显示带有重置时间的横幅("resets 3pm")。什么都不会丢失 — 一旦限制重置,就可以使用 `claude --resume ` 恢复会话。unsnooze 正是自动为每个停止的会话执行此操作。
### 那 Codex 呢 — "You've hit your usage limit. Try again at …"?
原理相同:ChatGPT 计划的 Codex 有 5 小时和每周的窗口。Codex TUI 在横幅出现后停留在 composer 处,因此 unsnooze 要么在实时窗格中输入,要么在它从横幅解析出的重置时间使用 `codex resume ` 重新打开会话。
### 这会绕过速率限制吗?
不会。unsnooze 会像你一样准确地等待重置,恢复一次,并验证限制是否确实已解除。它取代的是凌晨 4 点的闹钟,而不是限制。
### 我该如何更新,我怎么知道何时该更新?
`unsnooze update`(或 `npm i -g unsnooze`)。unsnooze 每天最多检查一次 npm 注册表,并告诉你何时存在更新的版本(CLI 命令后的一行文字,以及每个版本的一个桌面通知);更新后,下一条命令会显示更新日志中的一小段"新功能"。这只是一个简单的注册表 GET 请求,不包含任何身份识别信息 — 使用 `unsnooze config set updateCheck off` 关闭它。
### 如果一个会话停止时,另一个会话更改了仓库怎么办?
unsnooze 会在停止时对工作区(HEAD + 未提交状态)进行指纹识别,并在唤醒时重新检查。默认情况下,会话仍然会恢复,但唤醒消息会包含更改的内容("HEAD abc1234 → def5678 — re-read before continuing")。将 `workspaceGuard` 设置为 `pause` 可挂起此类会话,等待手动执行 `unsnooze resume-now`(它会打印差异统计信息),或者设置为 `off` 以禁用检查。
### 为什么恢复一个庞大的会话消耗了我这么多配额?
因为 prompt 缓存过期,而不是因为 unsnooze。提供商会缓存会话的上下文,因此每一轮只需为新内容支付全价 — 但该缓存只能存活约 5 分钟。在限制下停止数小时后,缓存早已消失,因此**第一条**唤醒消息(unsnooze 的消息,或手动输入的 "continue" — 无论哪种方式成本相同;唤醒消息本身约为 30 个 token)会使 API 以完全未缓存的全价重新读取整个对话。一个约 150k token 的会话,如果运行在顶级模型上,在唤醒的那一刻,在任何新工作发生之前,就会消耗掉新的 5 小时窗口中的一大块。
真正有帮助的做法是:在触及限制*之前*执行 `/compact`(或使用交接摘要启动全新的会话),并保持通宵会话精简。
unsnooze 的做法:`contextGuard` 设置会在唤醒之前根据会话转录估算上下文大小。`inform`(默认)恢复并通知你代价;`pause` 会将超过 `contextGuardTokens`(默认 100k)的会话挂起,直到你通过 `unsnooze resume-now` 做出决定;估算结果也会在 `unsnooze status` 中的每个会话旁边显示(`ctx ~152k tok`)。
### 如果我的笔记本电脑休眠或终端关闭了,它还能工作吗?
是的 — 重置时间作为绝对时间戳存储,并每 30 秒检查一次,因此睡过头重置时间的笔记本会在下一个节拍恢复,而死去的窗格会在新的多路复用器窗格中根据会话 id 重新打开。
## 开发
```
npm test # unit tests (node:test)
./scripts/e2e-simulate.sh # full detect → wait → re-open cycle in a
# scratch tmux session (no real limits needed)
bash -n scripts/e2e-zellij.sh # syntax-check the reserved-session Zellij smoke test
```
## 许可证
[MIT](LICENSE)标签:AI编程助手, MITM代理, tmux, 任务调度, 开发辅助工具, 暗色界面, 网络调试, 自动化, 自定义脚本