Fix Codex fork session reporting with standalone startup
This commit is contained in:
@@ -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
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user