Conversation
两套机制都在"确保 Hub 在运行",互相不知道对方存在:
1. launchd(com.forge-hub.plist,RunAtLoad + KeepAlive)
2. hub-client 的 ensureHubRunning():探测不可达就 spawn(..., {detached:true}) + unref()
第 2 条造出的进程不是 launchd 的子进程。它抢到端口后:
- launchd 每 ThrottleInterval(30s) 试着拉起自己的实例,撞端口崩掉,无限循环,
hub-stderr.log 里刷满 "Failed to start server. Is port 9900 in use?"
- `forge-hub sync` 的 `launchctl bootout` 杀不到它(不归 launchd 管),随后的
bootstrap 同样撞端口失败,而 `log("✓ Hub 已重启")` 是无条件打印的
净效果:**sync 报告重启成功,实际跑的仍是旧代码**。实测一台机器上 Hub 进程连续
存活 4 天 6 小时,跨越多次 sync 从未被替换;`ps -o ppid` 显示它的父进程是某个
Claude Code session 的 hub-channel.ts,不是 launchd。
修复分两处:
**hub-client(根因)**:新增 hub-starter.ts,装了 plist 时走
`launchctl kickstart` 让 launchd 启动,保证 Hub 始终是 launchd 的子进程;
kickstart 失败(service 未 bootstrap)则回退到原来的 detached spawn。
非 macOS 或没装 plist 时行为完全不变。
**cli.ts sync(止损)**:新增 waitUntil(),把"命令被接受"和"状态真的变了"分开。
bootout 之后轮询确认 Hub 真的下线,等不到就明确报告"它多半不是 launchd 启动的、
bootout 管不到"并给出手动步骤,**不再打印已重启**;bootstrap 之后轮询确认 Hub
恢复响应,成功消息改为有条件打印。
测试:
- hub-client/hub-starter.test.ts(4):darwin+plist→launchd;darwin 无 plist→spawn;
linux 有/无 plist 均→spawn
- cli.test.ts 新增 waitUntil(4):立即成立不轮询;中途翻转;超时返回 false;
支持 async predicate
验证:
- (cd hub-client && bunx tsc --noEmit && bun test) → 7 pass / 0 fail
- bun test cli.test.ts → 7 pass / 0 fail
- (cd hub-server && bun test) → 356 pass / 0 fail
- (cd forge-engine && bun test) → 32 pass / 0 fail
- bun hub-test-harness/harness.ts → 8/8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pGyNVmhbA1Z1vRoxLZbFe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
动机
两套机制都在"确保 Hub 在运行",互相不知道对方存在:
com.forge-hub.plist,RunAtLoad+KeepAliveensureHubRunning()探测到 Hub 不可达就自己启动:第 2 条造出的进程不是 launchd 的子进程。它抢到端口之后:
ThrottleInterval(30s)尝试拉起自己的实例,撞端口崩掉,无限循环。hub-stderr.log里刷满Failed to start server. Is port 9900 in use?forge-hub sync的launchctl bootout杀不到它(不归 launchd 管),随后的bootstrap同样撞端口失败——而log("✓ Hub 已重启")是无条件打印的净效果:sync 报告重启成功,实际跑的仍是旧代码。
实测:一台机器上 Hub 进程连续存活 4 天 6 小时,跨越多次
sync从未被替换:ps -o ppid显示它的父进程是某个 Claude Code session 的hub-channel.ts,不是 launchd。尝试手动纠正也失败了三次,失败模式完全一致——只要有一个活着的 Claude Code session,ensureHubRunning()就会在 Hub 消失的毫秒内重新 spawn,比launchctl bootstrap和launchctl kickstart都快:也就是说,在装了 launchd plist 的机器上,只要有 CC session 在跑,Hub 就不可能归 launchd 管,而且无法从外部纠正。
改动概要
hub-client(根因) — 新增
hub-starter.ts:装了 plist 时走launchctl kickstart,保证 Hub 始终是 launchd 的子进程;kickstart 失败(service 未 bootstrap)则回退到原来的 detached spawn。cli.ts sync(止损) — 新增
waitUntil(),把"命令被接受"和"状态真的变了"分开:bootout之后轮询确认 Hub 真的下线;等不到就明确报告"它多半不是 launchd 启动的、bootout 管不到"并给出手动步骤,不再打印已重启bootstrap之后轮询确认 Hub 恢复响应,成功消息改为有条件打印测试
hub-client/hub-starter.test.ts(4):launchdspawnspawnspawncli.test.ts新增waitUntil(4):立即成立时不再轮询;中途翻转能捕获;超时返回false;支持 async predicate。超时那条是关键——没有它,调用方会把"没等到"当成"成功了",正好是本 PR 要修的病。影响范围
hub-client/hub-channel.ts的ensureHubRunning()(新增hub-starter.ts决策函数)和cli.ts的sync(新增waitUntil());hub-server 不涉及。chooseHubStarter返回spawn,走原路径)。launchctl kickstart;kickstart 失败仍回退 spawn,所以最坏情况等于改动前。sync的输出语义变化:以前无条件打印「✓ Hub 已重启」,现在只在确认 Hub 真下线又真回来时才打印;等不到会给出手动步骤。依赖旧输出文案做判断的脚本需要注意。self-test 结果
grep -rE '@im\.wechat|ou_[a-f0-9]{16,}|sk-ant-…|sk-…|[0-9]{9,10}:[A-Za-z0-9_-]{35}'对本 PR 改动文件 → 无命中。