diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md index 765a656dc..91177506f 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md @@ -118,16 +118,21 @@ Open gaps before this binding is a promoted production default: (`@deepseek-ai/dsh-tool-fs`, `@deepseek-ai/dsh-tool-bash`). Without them a live model can answer but cannot act, and the Turn ends in a validation failure rather than in work. -- the host-mode plan still maps the unattended intent to the compatibility path: - `isolated_headless_turn` carries `turn_host: generic-cli` - (`loopx/host_mode_planner.py`), so the `loopx turn plan` command it prints - names `--host generic-cli` instead of the selected `dsh` default recorded above, - which is correct as a labelled rollback path but is not labelled as one. The - plan's `--host-identity` list deliberately covers visible - hosts only, because a headless-only host such as `dsh` cannot own a visible - session; the unattended mapping itself still needs a decision between naming - the resolved default, offering a `dsh` variant, or labelling the emitted - command as the rollback path. +- the host-mode plan used to map the unattended intent to the compatibility + path: `isolated_headless_turn` carried `turn_host: generic-cli` + (`loopx/host_mode_planner.py`), so the `loopx turn plan` command it printed + named `--host generic-cli` instead of the selected `dsh` default recorded + above, which is correct as a labelled rollback path but was not labelled as + one. **Decided and shipped:** the plan takes the "write out the resolved + default" option. The preview command pins no host, the pinned compatibility + variant is reported as `plan_command_rollback`, and the typed + `turn_mapping.host_selection` states which of the two a command is; targets + that genuinely need a visible identity (transitions into `visible_tui`) still + pin their host. `docs/reference/protocols/host-mode-plan-v0.md` defines + `turn_mapping.host` as the mode's declared host and scheduler context rather + than as an already-resolved concrete host. The plan's `--host-identity` list + still covers visible hosts only, because a headless-only host such as `dsh` + cannot own a visible session. ## Evidence Baseline diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md index b1fabf171..0a6417d83 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md @@ -97,13 +97,17 @@ CLI 的情况下把管家悄悄换成 operator 模型。 - LoopX 的 DSH Turn 组合必须显式列出托管动作所需的工具行 (`@deepseek-ai/dsh-tool-fs`、`@deepseek-ai/dsh-tool-bash`)。缺少它们时,真实模型 只能作答而无法动手,Turn 会以验证失败而不是产出工作结束。 -- 宿主模式计划仍把无人值守意图映射到兼容路径:`isolated_headless_turn` 的 +- 宿主模式计划此前把无人值守意图映射到兼容路径:`isolated_headless_turn` 的 `turn_host` 取 `generic-cli`(`loopx/host_mode_planner.py`),因此它打印的 `loopx turn plan` 命令写的是 `--host generic-cli`,而不是上文记录的已选 `dsh` - 默认值;作为回滚路径本身没错,但没有被标注为回滚路径。该计划的 - `--host-identity` 列表只覆盖可见宿主是有意为之——像 - `dsh` 这种仅 headless 的宿主无法拥有可见会话;但无人值守映射本身仍需在"写出解析 - 后的默认值 / 提供 `dsh` 变体 / 把该命令标注为回滚路径"之间做出决定。 + 默认值;作为回滚路径本身没错,但没有被标注为回滚路径。**已决并已落地:**计划采用 + "写出解析后的默认值"这一支——预览命令不再 pin 任何 host,pin 死的兼容变体报为 + `plan_command_rollback`,类型化的 `turn_mapping.host_selection` 说明该命令属于 + 哪一种;真正需要可见身份的路径(转入 `visible_tui` 的 transition)仍然 pin 具体 + 宿主。`docs/reference/protocols/host-mode-plan-v0.md` 已把 `turn_mapping.host` + 定义为该模式的声明宿主与调度上下文,而不是"具体宿主已经解析完成"。该计划的 + `--host-identity` 列表仍只覆盖可见宿主,因为像 `dsh` 这种仅 headless 的宿主无法 + 拥有可见会话。 ## 证据基线 diff --git a/docs/reference/protocols/host-mode-plan-v0.md b/docs/reference/protocols/host-mode-plan-v0.md index 665113907..b1cb25e3b 100644 --- a/docs/reference/protocols/host-mode-plan-v0.md +++ b/docs/reference/protocols/host-mode-plan-v0.md @@ -142,6 +142,23 @@ Each `mode_options[]` entry includes the connector id, readiness, required host capabilities, Turn mapping when one exists, scheduler execution context, quota guard command, and required proofs. +`selected_turn_mapping` fields mean: + +- `host` is the mode's **declared** host: the scheduler context the readiness + statement and capability requirements are written against. It is not a claim + that this concrete host has already been resolved for the run; the runtime + resolves the concrete host (including a credential- or configuration-resolved + default) when `plan_command` runs. +- `host_selection` is `resolved_default` when the command deliberately leaves + host resolution to `loopx turn plan`/`run-once`, and `pinned` when the command + carries an explicit `--host`. +- `plan_command` is the command to run for this mapping. When `host_selection` + is `resolved_default` it pins no host, so it cannot freeze the compatibility + adapter path as the product default. +- `plan_command_rollback` is the pinned compatibility variant for an operator + who deliberately wants that path instead of the resolved default. It is + present only when the mapping resolves its host. + ## Functional Points The selector provides four concrete functions: @@ -150,7 +167,8 @@ The selector provides four concrete functions: users and agents to infer visible/headless/gateway/timer behavior manually. 2. **Turn mapping:** for unattended execution, print the exact `loopx turn plan` preview that preserves host, execution mode, scheduler owner, agent id, and - available capabilities. + available capabilities, while leaving the concrete host to the runtime's + resolved default and reporting the pinned variant as the rollback command. 3. **Readiness surface:** report which advertised capabilities are missing before a mode can be trusted. 4. **Safe handoff plan:** name transitions such as visible bootstrap to diff --git a/examples/host-mode-plan-smoke.py b/examples/host-mode-plan-smoke.py index a31a9276d..8c3f4b223 100755 --- a/examples/host-mode-plan-smoke.py +++ b/examples/host-mode-plan-smoke.py @@ -348,6 +348,23 @@ def test_hybrid_requires_two_ready_modes_and_names_handoffs() -> None: assert transition["preserves_agent_id"] is True, transition assert transition["spends_quota"] is False, transition assert "--agent-id codex-main-control" in transition["guard_command"], transition + # The transition policy is the same rule as the mode preview: a target that + # resolves its host must not pin the compatibility adapter path, while a + # target that needs a visible identity still pins it. + by_id = {item["transition"]: item for item in two_ready["transitions"]} + for transition_id in ( + "visible_bootstrap_to_isolated_headless_turn", + "im_gateway_to_isolated_headless_turn", + ): + command = by_id[transition_id]["target_turn_plan_command"] + assert "--host" not in command, (transition_id, command) + for transition_id in ( + "isolated_headless_turn_to_visible_tui_escalation", + "shell_service_to_visible_tui_escalation", + ): + command = by_id[transition_id]["target_turn_plan_command"] + assert "--host codex-cli" in command, (transition_id, command) + assert "--execution-mode interactive-visible" in command, (transition_id, command) def test_identity_gate_and_no_spend_boundary() -> None: