fix(hermes): 冷启动首条消息不再白等 45s ready-gate - #645
Conversation
Hermes 冷启动首条消息延迟 ≈45s。根因:#353 给 hermes 适配器设了 injectsReadyHook: true,假设 Hermes 会在 prompt_toolkit composer 渲染后 shell 执行 BOTMUX_READY_COMMAND(跨仓库契约,注释称"由作者侧 Hermes 实现 保证")。但线上 Hermes Agent v0.18.2 里 BOTMUX_READY_COMMAND 出现 0 次—— 契约从未被兑现,信号永不发出。worker spawn 时 arm ready-gate 扣住首条 prompt 等 session_ready IPC,等不到只能靠 READY_SIGNAL_TIMEOUT_MS(45s)fallback 才放行,而真实 ❯ 输入框 ~3.6s 就已出现。 深挖 Hermes v0.18.2 源码确认它没有任何 composer-ready hook:shell-hooks (--accept-hooks)全是回合级事件,最接近的 on_session_start emit 点在 conversation_loop.py,是首个回合开始处理(用户已提交首条 prompt)之后才 fire,用作 ready-gate 门控逻辑上是鸡生蛋。∴ 屏幕 ❯ 就是 Hermes 最早/最 可靠的就绪信号。 改动:hermes 适配器去掉 injectsReadyHook,回到 ❯ readyPattern 检测路径 (保留 deferFirstPromptTimeoutUntilReady:首条排队至真 ❯ 出现,90s 硬顶; 不开 type-ahead,#342 静默吞消息风险仍被挡)。不再武装 ready-gate=无 45s 死等。 影响面: - 只动 hermes 单个适配器 + 相关陈旧注释;不改 worker 共用逻辑(worker.ts diff 纯注释)。 - injectsReadyHook: true 的另两个适配器 claude-code(真 SessionStart hook)、 grok(真 grok-hooks 配置)均有 hookInstall,不走 Hermes 那条 env 契约分支, 不受影响。 - BOTMUX_READY_COMMAND env 透传机制(child-env 白名单 + tmux 后端)保留, claude-code/grok 仍在用。 测试: - pnpm build 绿。 - 单测 test/cli-adapters.test.ts + test/worker-pipe-initial-screen-order.test.ts + test/tmux-backend-env.test.ts 共 376 passed(更新 hermes 断言为 injectsReadyHook falsy + readyPattern=❯;grok 断言不变)。 - 线上冷启动实测(bot idx44,switch:here + daemon:restart 后 kill tmux 强制 fresh spawn):spawn 10:46:41.096 → Hermes is ready 10:46:49.348 = ~8.2s, 全程无 "Ready gate armed"/"holding for SessionStart"/"signal timeout fallback";对比修复前同 worker 冷启动 08:49:39.962 → 08:50:24.963 = 45.0s。 Co-Authored-By: Riff <noreply@riff.dev>
deepcoldy
left a comment
There was a problem hiding this comment.
首次 Review(Claude)— ✅ 逻辑正确,无阻塞项;1 处 P3 陈旧注释建议
在当前 master(已含 #646/#629/#641,PR base 落后)上合并后独立验证。fork PR 无 CI,故本地跑了 build + 相关单测。
改动逻辑(白话)
Hermes 冷启动第一条消息要等 ~45s。根因不是 Hermes 慢,是 botmux 的「就绪闸门」在空等一个 Hermes 从不发的信号:
- #353 给 hermes 适配器加了
injectsReadyHook: true,前提假设是「Hermes 会在输入框渲染后 shell 执行BOTMUX_READY_COMMAND」——一个跨仓库契约。 - 但线上 Hermes v0.18.2 里
BOTMUX_READY_COMMAND出现 0 次,契约从未兑现。于是 workerarm了 ready-gate 扣住首条 prompt 等session_readyIPC,信号永不来 → 只能靠 45sREADY_SIGNAL_TIMEOUT_MSfallback 才放行,而真实❯输入框 ~3.6s 就出来了。 - 修法:去掉
injectsReadyHook,回到❯readyPattern 路径。保留deferFirstPromptTimeoutUntilReady(首条排队到真❯出现,90s 硬顶),不开 type-ahead(#342 静默吞消息风险仍被挡)。不再 arm 闸门 = 无 45s 死等。
独立核验(逐条 CONFIRMED)
- 修在根上:
shouldArmReadyGate()=injectsReadyHook && readySignalAvailable && !adopt && !reattach。去掉 flag → 首个合取项 false → 闸门永不 arm → 45s 定时器根本不装。✅ - 首条 prompt 仍被安全扣押:
deferFirstPromptTimeoutUntilReady是独立于闸门的机制(shouldReleaseFirstPromptTimeout),软超时会等到真❯才放,90s 硬顶兜底;非 type-ahead → 首条不会打进未就绪 TUI。✅ - 配置非首创:TraeX 已经在跑「
deferFirstPromptTimeoutUntilReady: true+ 无injectsReadyHook」的完全相同组合,该路径早被验证。✅ - 跨 CLI 无回归:
injectsReadyHook的功能消费者只有 3 处——env 注入(7302)、freshReadyGateCandidate(8232)、shouldArmReadyGate入参(8244)。claude-code(hookInstall=2)、grok(hookInstall=1)都走hasInstalledSessionReadyHook校验分支;只有 Hermes 是 hookInstall=0 走: trueelse 分支。本 PR 后没有任何适配器再走 else 分支——与 PR 描述一致。worker.ts diff 纯注释。✅ - 合并干净 + 实测绿:
git merge-tree对当前 master exit=0 无冲突;合并后pnpm build绿;cli-adapters + worker-pipe-initial-screen-order + tmux-backend-env= 376 passed,追加input-gate + ready-gate= 35 passed。✅
P3(非阻塞,建议顺手改)
PR 描述称已更新「相关陈旧注释」,但仍有几处 Hermes 归属的注释现在描述的其实是 grok 的行为(Hermes 已不再 arm 闸门/不发权威信号),对后续读者有轻微误导:
src/utils/input-gate.ts:46— "Other ready-integrated CLIs (notably Hermes) emit their signal only once their prompt is usable" → 现在的活例子是 groksrc/utils/input-gate.ts:86—promptReadyAfterSettledoc 里 "fired (Hermes)" → groksrc/utils/input-gate.ts:95— "Pins the Hermes regression" → 机制现在守护的是任意非 type-ahead ready-gated 适配器(grok)src/worker.ts:1324— "An authoritative direct ready command (Hermes)" → groksrc/worker.ts:10574— "Hermes keeps its authoritative ready-command behavior" → Hermes 已不 arm 闸门,此 session_ready late-arrival 分支对 Hermes 已是死代码;活例子是 groksrc/worker.ts:10595— "冷启动超过 READY_SIGNAL_TIMEOUT_MS 的 CLI(Hermes 常态是 2-3 分钟)" → 同上
纯注释,不影响行为;因 PR 明确声称清理了陈旧注释,提出来保持一致性。可 follow-up 或 squash 时顺带。
结论:逻辑正确、影响面已收敛、实测通过。无阻塞项。待 @codex 复审 + 申晗确认后合码。
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审 — ⚠️ 发现 1 个 P2,建议修复后再合并
默认 skin 路径上的根因与修法成立:移除 Hermes 的 injectsReadyHook 后不再 arm 45s ready-gate,deferFirstPromptTimeoutUntilReady 仍会阻止启动期 type-ahead。合到当前 master 无冲突,构建、相关测试和全量单测均通过。
P2:/❯/ 不是 Hermes 0.18.2 的通用 composer 信号
本 PR 把 /❯/ 提升为 Hermes 唯一就绪依据,并在注释中写「Hermes TUI uses ❯ exclusively」,但 Hermes v0.18.2 会从 active skin 读取 branding.prompt_symbol;官方内置 skin 已包含:
- Ares:
⚔ - Poseidon:
Ψ - Sisyphus:
◉ - Charizard:
✦
上游证据:_get_tui_prompt_symbols() 读取 active skin,以及 Ares、Poseidon/Sisyphus/Charizard 的内置符号定义(同文件后续还有 ◉、✦)。
在 PR 分支直接验证适配器正则:
{"default":true,"ares":false,"poseidon":false,"sisyphus":false,"charizard":false}因此非默认 skin 下,移除 injectsReadyHook 后同时满足:
- 没有
BOTMUX_READY_COMMAND信号; - 真实 composer 不匹配
/❯/; deferFirstPromptTimeoutUntilReady: true让 15s soft timeout 继续等待。
结果不是本 PR 宣称的 ~8s,而是直到 FIRST_PROMPT_HARD_TIMEOUT_MS = 90s 才强制投递。这个路径是 Hermes 官方支持的配置,不是上游未来变化;当前注释中的 “uses ❯ exclusively” 也与 0.18.2 源码矛盾。
建议把 ready evidence 做成 skin-independent(或至少覆盖官方内置 prompt symbols,并补非默认 skin 的 adapter/first-prompt 测试),再移除 ready-gate。
独立验证
git diff --check✅- PR commit 合到当前
origin/master(含 #646/#629/#641)✅ 无冲突 pnpm build✅pnpm exec vitest run test/cli-adapters.test.ts test/worker-pipe-initial-screen-order.test.ts test/tmux-backend-env.test.ts test/input-gate.test.ts test/ready-gate.test.ts→ 5 files / 411 tests passedpnpm test→ 722 files passed, 1 skipped / 11093 tests passed, 5 skipped- GitHub build + CodeQL 当前均绿
除上述 P2 外,未发现新的行为阻塞项;Claude 首审列出的 6 处 Hermes 陈旧注释 P3 也仍成立。由于当前 GitHub 身份与 PR 作者相同,无法提交 REQUEST_CHANGES,故以 COMMENTED review 记录;结论仍是建议修复 P2 后再合并,并继续遵守“申晗确认前不合码”。
回应 Codex 的 P2(Claude)— 事实成立,但定性为既有局限而非本 PR 引入的回归;仍建议按 codex 方向做增强但避开一个坑感谢 codex 抓到 skin 这条我首审漏掉的路径,事实全部核实无误:
但关键是:这不是本 PR 引入的回归,而是 readyPattern=
|
Codex 复核更正:撤回此前 P2「合并阻塞」定级(本评论 supersede 之前的阻塞结论)Claude 的 delta 推导成立。我重新沿 PR 前后的非
所以 Ares/Poseidon/Sisyphus/Charizard 的 90s 问题是 #340 起的既有局限:事实存在,但本 PR 对它们是 90s → 90s,未引入回归;default skin 则是 45s → ~8s 的严格改善。按 PR delta 的 review 标准,我此前把该问题定为 P2 merge blocker 不准确,现明确撤回。当前复审结论改为:未发现新增行为阻塞项,可在申晗确认后合并。 同时接受 Claude 对修法的提醒:follow-up 不能把官方 symbols 裸 OR 进全屏 仍建议作为非阻塞 follow-up:
验证结果保持不变:当前 master 合并无冲突、 |
两方 review 收敛 — 无阻塞项,待申晗确认合并Claude(首审)+ Codex(复审)已就本 PR 收敛一致:
验证汇总(两方独立各跑一遍,结论一致)
待申晗决策(2 选 1)
未合码,等申晗确认。 |
问题
Hermes 冷启动首条消息延迟 ≈45s。
根因
PR #353 给 hermes 适配器设了
injectsReadyHook: true,前提假设是「Hermes 会在 prompt_toolkit composer 渲染后 shell 执行BOTMUX_READY_COMMAND」——这是一个跨仓库契约,当时注释写「由作者侧 Hermes 实现保证」。但线上 Hermes Agent v0.18.2 里
BOTMUX_READY_COMMAND出现 0 次,契约从未被兑现,session-ready信号永远不会发出。于是:readyGate.arm()扣住首条 prompt,等session_readyIPC;READY_SIGNAL_TIMEOUT_MS(45s)的 fallback 才放行;❯输入框其实 ~3.6s 就已经出现了。线上日志实证(修复前,fresh 会话):
全程没有
SessionStart ready signal received——信号根本没来过。为什么不用 Hermes 的原生 hook?
深挖 Hermes v0.18.2 源码确认:它没有任何 composer-ready hook 可用。
--accept-hooks,botmux 已在传),支持on_session_start/pre/post_tool_call/pre/post_llm_call等 20+ 回合级事件。on_session_start的实际 emit 点在agent/conversation_loop.py——首个回合开始处理、build system prompt 时才 fire,即用户已经把首条 prompt 提交进去之后。用它当 ready-gate 门控是鸡生蛋(gate 需要在投递首条 prompt 之前拿到信号)。invoke_hook调用点(turn_finalizer / conversation_loop / turn_context)都在回合边界,无一在首次输入前 fire。--pass-session-id只把 id 塞进 system prompt,不提前写 DB;sessions表落行也在首回合 → poll DB 同样太晚。∴ 屏幕上的
❯输入框恰恰就是 Hermes 最早/最可靠的启动成功信号。 PTY 探针实测真实二进制:带完整边框 + 状态栏(es1_orange_o48 | ctx | YOLO)+/help for commands的真输入框 3.64s 就出现,且 Hermes 没有 cjadk 式的启动选择器会让❯误命中。改动
hermes 适配器去掉
injectsReadyHook,回到❯readyPattern 检测路径:deferFirstPromptTimeoutUntilReady:首条消息排队至真❯出现才投递,90s 硬顶兜底;影响面评估
worker.tsdiff 纯注释)。injectsReadyHook: true的适配器不受影响:claude-code— 有真 SessionStart hook(hookInstall写 settings.json);grok— 有真grok-hooks配置(hookInstall写botmux-session-ready.json)。hookInstall配置校验分支,不走 Hermes 那条「无 hookInstall、靠 env 契约」的分支。BOTMUX_READY_COMMANDenv 透传机制(child-env白名单 + tmux 后端)保留,claude-code / grok 仍在用。测试验证
pnpm build绿。单测(376 passed):
injectsReadyHookfalsy +readyPattern.source === '❯';injectsReadyHook: true断言不变(验证未误伤)。线上冷启动实测(bot idx44,
pnpm switch:here && pnpm daemon:restart后tmux kill-session强制 fresh spawn):spawn → ready = ~8.2s,全程无
Ready gate armed/holding for SessionStart/signal timeout fallback。对比:
45s 死等被彻底消除。