Fix Codex fork session reporting with standalone startup

This commit is contained in:
2026-10-23 03:21:59 +00:00
parent 8cac4370bf
commit fbd3da715a
3 changed files with 26 additions and 4 deletions
+7 -2
View File
@@ -120,16 +120,21 @@ Sessions 弹窗仍使用 `Ctrl+A g`;两个菜单都使用 Hub 外层的 `Ctrl+
Fork 复制的是 **Codex / Claude conversation**,并保留正在运行的原 Agent 和原 Tab。
它在同一个 Herdr Workspace、相同 cwd 新建 Tab,使用当前 pane 已报告的 conversation ID:
Codex 执行 `codex fork UUID`;Claude 执行 `claude --resume SESSION_ID --fork-session`。
Codex 执行 `codex --no-daemon fork UUID`;Claude 执行 `claude --resume SESSION_ID --fork-session`。
新 Agent 被 Herdr 确认为就绪后才切换焦点。它不会新建外层 Herdr named session,
也不会创建 Git worktree;两个 Agent 会编辑同一工作目录,需自行协调并发修改。
支持本地 default / named session,以及通过 SSH 控制的远端 native / Windows SSH endpoint。
控制目标取自触发菜单的 Hub window metadata,不使用 popup 继承的 Herdr socket 或 pane 环境变量。
目标必须运行 Herdr 0.9+,并安装对应 Agent;当前 pane 必须已报告可用的 Agent session ID。
目标必须运行 Herdr 0.9+,并安装对应 Agent(Codex 需支持 `--no-daemon`);当前 pane 必须已报告可用的 Agent session ID。
Fork 显示灰色时,菜单会说明原因;按 `Ctrl+R` 可重新读取当前 Agent 信息。
若提示尚未上报会话 ID,在该主机检查 `herdr integration status`,确认对应 Agent 的集成已加载。
识别出 Codex / Claude 并不代表已经取得 conversation ID;Hub 不会根据目录猜测会话。
Codex Fork 使用 `codex --no-daemon fork UUID`,让新进程和集成钩子使用新 pane 的环境。
共享 Codex 后台服务可能沿用已关闭 pane 的 `HERDR_PANE_ID` 或其他 Session 的 socket,
即使集成显示已安装,也可能上报失败。若已有会话因此没有 ID,可在新 Herdr Tab 中运行
`codex --no-daemon fork`,从选择器选择原会话;原会话会保留。Codex 0.163.0 在提交首条消息时
才触发会话上报,因此新建、恢复或 Fork 后需先发送一条消息,再打开菜单或按 `Ctrl+R`。
远端控制使用非交互 SSH(需要已信任的主机密钥和可用认证);Windows 需要 PowerShell。
启动 blocked、失败、超时或响应不完整时,菜单保留错误信息,**不会自动关闭任何 Tab**。
超时并不代表远端没有创建或启动成功,请先检查提示中的新 Tab,再重新打开菜单重试。
+10 -1
View File
@@ -91,7 +91,9 @@ class ForkSource:
@property
def arguments(self) -> list[str]:
if self.kind == "codex":
return ["fork", self.session_id]
# A shared Codex server can retain another pane's Herdr environment.
# Keep the fork and its session-reporting hooks in the new pane.
return ["--no-daemon", "fork", self.session_id]
return ["--resume", self.session_id, "--fork-session"]
@@ -116,9 +118,16 @@ def parse_snapshot(payload: object) -> ForkSource:
raise ControlError(f"暂不支持 Fork {kind};支持 Codex / Claude")
session = pane.get("agent_session")
if not isinstance(session, dict):
recovery = (
"\n若 Codex 集成已安装,共享后台服务可能沿用了旧 pane 环境。"
"可在新 Herdr Tab 中运行 codex --no-daemon fork,选择原会话;"
"发送首条消息后按 Ctrl+R 重读。"
if kind == "codex" else ""
)
raise ControlError(
f"当前 {kind.capitalize()} 尚未向 Herdr 上报会话 ID,暂时无法 Fork。\n"
f"在目标主机检查 herdr integration status;确认集成已加载后按 Ctrl+R 重读。"
f"{recovery}"
)
if session.get("kind") != "id" or session.get("agent") != kind:
raise ControlError("当前 Agent 尚未报告可 Fork 的 conversation ID")
+9 -1
View File
@@ -51,9 +51,17 @@ class SnapshotTests(unittest.TestCase):
with self.assertRaisesRegex(ControlError, "尚未向 Herdr 上报会话 ID") as caught:
parse_snapshot(payload)
self.assertNotIn("需要 Herdr 0.9", str(caught.exception))
self.assertIn("codex --no-daemon fork", str(caught.exception))
def test_missing_claude_session_does_not_suggest_codex_recovery(self):
payload = snapshot("claude")
del payload["result"]["snapshot"]["panes"][0]["agent_session"]
with self.assertRaisesRegex(ControlError, "尚未向 Herdr 上报会话 ID") as caught:
parse_snapshot(payload)
self.assertNotIn("codex --no-daemon", str(caught.exception))
def test_focused_pane_and_exact_native_arguments(self):
for kind, args in (("codex", ["fork", SESSION_ID]),
for kind, args in (("codex", ["--no-daemon", "fork", SESSION_ID]),
("claude", ["--resume", SESSION_ID, "--fork-session"])):
with self.subTest(kind=kind):
source = parse_snapshot(snapshot(kind))