diff --git a/README.md b/README.md index 08d008d..06c349a 100644 --- a/README.md +++ b/README.md @@ -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,再重新打开菜单重试。 diff --git a/herdr_hub/control.py b/herdr_hub/control.py index e5dc95d..a37ad2d 100644 --- a/herdr_hub/control.py +++ b/herdr_hub/control.py @@ -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") diff --git a/tests/test_control.py b/tests/test_control.py index 5f11380..83a7839 100644 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -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))