From 999245f60263847cd81ac65cbc94432b9298e6e8 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:24:59 +0800 Subject: [PATCH 1/2] fix(dashboard): resolve the steward chat runtime from the channel The chat-runtime picker preferred a discovered Codex adapter over the executor the machine declares for the steward, so a channel resolving to the managed host still displayed "Codex" in the header trigger and above the composer while the capability chip resolved "dsh". The client is expected to send no endpoint until the operator picks one, so the picker has to show the channel's own resolution rather than a CLI this machine merely has installed. The manager context now defaults to the declared steward executor when the control plane reports one that maps to a discovered adapter, and keeps the shipped Codex default when the machine declares nothing. Goal contexts are unchanged, an explicit operator pick still wins, and no endpoint is sent when no pick was made. The execution-chip scenario proves the new resolution and the undeclared-machine parity, with a fixture seam for declaring runtime adapters. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../dashboard/src/views/dashboard-page.tsx | 14 ++++- .../execution-chip.mjs | 58 ++++++++++++++++++- .../personal-workspace-browser/fixture.mjs | 4 +- .../typed-actions.mjs | 2 +- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index 3ef44b67d0..e2ba346c63 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -1486,8 +1486,20 @@ function PersonalGoalHome({ const defaultAgentId = discoveredAgents.find((agent) => agent.label === "Codex" && agent.available)?.agentId ?? discoveredAgents.find((agent) => agent.available)?.agentId ?? "status-only"; + // The manager channel answers on the executor this machine declares for the + // steward, and the client is expected to send no endpoint until the operator + // picks one. The picker therefore has to show that same resolution: an + // executor merely discovered on this machine is not a reason to present + // itself as the steward's runtime, or the header chip, the composer and the + // answer would tell three different stories. + const declaredStewardEndpoint = managerChannelBinding?.executor_endpoint?.trim() ?? ""; + const stewardExecutorAgentId = declaredStewardEndpoint + ? discoveredAgents.find((agent) => agent.agentId === declaredStewardEndpoint)?.agentId + : undefined; + const agentDefaultForContext = (targetContextId: string) => + targetContextId === "manager" ? stewardExecutorAgentId ?? defaultAgentId : defaultAgentId; const [selectedAgents, setSelectedAgents] = useState>(readPersonalAgentSelections); - const selectedAgentId = selectedAgents[contextId] ?? defaultAgentId; + const selectedAgentId = selectedAgents[contextId] ?? agentDefaultForContext(contextId); const selectedAgent = selectAvailableChatAgent(agentOptions, selectedAgentId, defaultAgentId); const [agentMenuOpen, setAgentMenuOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); diff --git a/examples/personal-workspace-browser/execution-chip.mjs b/examples/personal-workspace-browser/execution-chip.mjs index 805ea66c13..72df23e75b 100644 --- a/examples/personal-workspace-browser/execution-chip.mjs +++ b/examples/personal-workspace-browser/execution-chip.mjs @@ -196,6 +196,62 @@ export const executionChipScenario = { coverageEntries.push(...await managed.close()); } + // The chat-runtime picker has to report the same resolution as the chip: a + // managed host declared for this machine is what will answer, so neither + // the header trigger nor the composer may keep advertising the CLI the + // machine merely happens to have installed. + const managedHostAdapters = [{ + adapter_kind: "deepseek_harness_segment", + agent_id: "dsh", + available: true, + display_name: "DeepSeek Harness (managed)", + interrupt: false, + resume: true, + streaming: false, + }]; + const stewardPicker = await openWorkspacePage(browser, url, { + apiOptions: { managerChannelBinding: selectedManagedBinding, runtimeAgents: managedHostAdapters }, + collectCoverage, + }); + try { + const pickerLabel = (await stewardPicker.page + .locator("div.personal-agent-select button.personal-select-trigger") + .innerText()).replace(/\s+/g, " ").trim(); + if (!pickerLabel.includes("DeepSeek Harness (managed)")) { + throw new Error(`Chat runtime picker ignored the declared steward executor: ${pickerLabel}`); + } + if (pickerLabel.includes("Codex")) { + throw new Error(`Chat runtime picker advertised a discovered CLI as the steward: ${pickerLabel}`); + } + const composerLabel = (await stewardPicker.page + .locator(".personal-channel-composer > span") + .first() + .innerText()).trim(); + if (composerLabel !== "DeepSeek Harness (managed)") { + throw new Error(`Composer named ${composerLabel} instead of the steward's declared executor`); + } + } finally { + coverageEntries.push(...await stewardPicker.close()); + } + + // A machine that declares no steward executor keeps the shipped default, + // so the parity above is a resolution the control plane asked for and not + // a new hard-coded preference. + const undeclaredSteward = await openWorkspacePage(browser, url, { + apiOptions: { runtimeAgents: managedHostAdapters }, + collectCoverage, + }); + try { + const pickerLabel = (await undeclaredSteward.page + .locator("div.personal-agent-select button.personal-select-trigger") + .innerText()).replace(/\s+/g, " ").trim(); + if (pickerLabel !== "Chat Codex") { + throw new Error(`An undeclared steward executor no longer used the shipped default: ${pickerLabel}`); + } + } finally { + coverageEntries.push(...await undeclaredSteward.close()); + } + // A managed host that cannot launch here names the missing fact, and never // claims the channel lacks a transport it now has. for (const [binding, expected] of [ @@ -306,7 +362,7 @@ export const executionChipScenario = { } return { coverageEntries, - note: "execution chip reports the selected executor, the credential it is billed to and the resolved model, names the steward channel's shipped default without claiming a credential branch, keeps a configured credential from moving it, names why a selected managed host cannot launch, and stays absent without a binding", + note: "execution chip reports the selected executor, the credential it is billed to and the resolved model, names the steward channel's shipped default without claiming a credential branch, keeps a configured credential from moving it, keeps the chat-runtime picker on the executor the machine declares for the steward, names why a selected managed host cannot launch, and stays absent without a binding", }; }, }; diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs index cb4ac87f1b..543623a8fc 100644 --- a/examples/personal-workspace-browser/fixture.mjs +++ b/examples/personal-workspace-browser/fixture.mjs @@ -288,7 +288,7 @@ function filterStatusFixtureToScope(fixture, statusGeneration, scope) { } } -export async function installApi(page, { goalSubagentConfigurationEnabled = true, initialActionProposals = [], managerChannelBinding = null } = {}) { +export async function installApi(page, { goalSubagentConfigurationEnabled = true, initialActionProposals = [], managerChannelBinding = null, runtimeAgents = null } = {}) { let turnCounter = 0; const runtime = page.__loopxRuntime ??= { actionProposals: new Map(), goalSubagentConfigurations: new Map(), larkConnections: [], messages: new Map(), sessions: new Map(), turnMessages: new Map() }; const actionProposals = runtime.actionProposals; @@ -1251,7 +1251,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true { agent_id: "codex", display_name: "Codex", adapter_kind: "codex_app_server", available: true, streaming: true, resume: true, interrupt: true }, { agent_id: "claude-code", display_name: "Claude Code", adapter_kind: "claude_code_cli", available: true, streaming: true, resume: true, interrupt: true }, { agent_id: "offline-agent", display_name: "Offline Agent", adapter_kind: "acp", available: false, streaming: false, resume: false, interrupt: false }, - ], + ].concat(runtimeAgents ?? []), }, status: 200 }); return; } diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 82118d902b..16999f6a86 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -1611,7 +1611,7 @@ export const typedActionsScenario = { await page.keyboard.press("Escape"); if (await agentListbox.isVisible().catch(() => false)) throw new Error("Agent menu did not close on Escape"); if (!(await agentSelect.evaluate((element) => element === document.activeElement))) throw new Error("Agent menu did not restore trigger focus"); - pass(14, "Codex remained the healthy default and the unavailable Agent option was disabled with explanation."); + pass(14, "With no declared steward executor the shipped Codex default held, and the unavailable Agent option was disabled with explanation."); await agentSelect.click(); const reopenedAgentListbox = page.getByRole("listbox", { name: "选择聊天 Runtime" }); await reopenedAgentListbox.getByRole("option", { name: "Claude Code", exact: true }).click(); From f1f72ac7b144db1c169c348497df27777b3066d3 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:25:00 +0800 Subject: [PATCH 2/2] docs(rfc): record the steward answer identity and runtime resolution Steward executor selection is a machine decision, so the RFC now states the two rules that keep the surfaces consistent: the transcript names the speaker while the capability chip reports the executor and model, and the chat-runtime picker resolves the manager context the way the channel owner does. The former smoke wording that described Codex as the default in general now names the condition it actually holds under. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../rfcs/harness-selection-dsh-pi-v0.md | 36 +++++++++++++++++++ .../rfcs/harness-selection-dsh-pi-v0.zh-CN.md | 26 ++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md index ef5ee95768..d8fbf83aa3 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md @@ -474,6 +474,42 @@ Validation: `tests/capabilities/test_steward_executor_machine_defaults.py`, `tests/capabilities/test_capability_configuration_ui.py`, and `examples/loopx-steward-channel-binding-smoke.py`. +### Steward Answer Identity and Runtime Selection (2026-09-16) + +Once a machine could declare its steward executor, the Dashboard was still +answering two different questions with one value: *who answered me* and *which +runtime served the turn*. The transcript titled a steward answer with whatever +the chat-runtime picker happened to hold, and the picker itself preferred a +`Codex` adapter whenever one was discovered on the machine -- so a channel +resolving to the managed host could still present itself as an individual CLI +login, and the header chip, the composer and the answer could each name a +different executor. + +Two rules now hold on the manager channel: + +* **Answer identity names the speaker.** The transcript labels a steward answer + `LoopX Manager` / `LoopX 管家` on every path that can produce one -- return + receipts, resumed history, recovery streaming, the streaming placeholder, the + completion fallback, interruption and failure. The executor and its model + stay in the machine-capability chip, which is the surface that reports them. + Goal channels keep naming the Goal's own Agent. +* **Runtime selection resolves the way the channel resolves.** The chat-runtime + picker follows the channel owner's precedence for the manager context: the + declared steward executor first, the shipped default only when the machine + declares nothing. A discovered adapter is never presented as the steward. + +An explicit operator pick still wins for that context, and the client still +sends no endpoint when the operator made no pick, so the create-session contract +in `loopx/chat_server.py` -- "each channel resolves its own default through its +own owner" -- is unchanged. An explicit pick is also the only path that moves +this channel off the declared executor, which keeps a discovered CLI from +silently rewriting a machine decision. + +Evidence: `examples/personal-workspace-browser-smoke.mjs` (`execution-chip` +scenario for the picker and composer resolution, `chat-recovery` scenario for +the answer identity), plus a live readback on the installed Dashboard where the +header chip resolved `dsh` while the picker previously reported `Codex`. + ## Steward Team Intake (2026-09-16) The steward answers questions. Since `2026-09-16` its shipped guidance also 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 36ebdd25d4..b037111d43 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 @@ -373,6 +373,32 @@ journal 与配额语义;B 作为上游接口出现时的低成本替代;只 `tests/capabilities/test_capability_configuration_ui.py`,以及 `examples/loopx-steward-channel-binding-smoke.py`。 +### 管家回答身份与 Runtime 选择(2026-09-16) + +机器可以声明管家执行器之后,Dashboard 仍在用一个值回答两个不同的问题:**谁在回答我**, +以及**哪个 runtime 跑了这一回合**。会话记录用聊天 Runtime 选择器当时持有的值给管家回答 +署名;而选择器本身只要在本机发现 `Codex` 适配器就优先选它——于是解析到托管宿主的通道 +仍可能把自己呈现成个人 CLI 登录,页头 chip、输入框与回答可以各说各的执行器。 + +管家通道上现在有两条规则: + +* **回答身份署名说话者。** 会话记录在**所有**能产生管家回答的路径上署名 + `LoopX 管家` / `LoopX Manager`:交接回执、恢复的历史、恢复流式、流式占位、 + 完成兜底、中断与失败。执行器与模型留在机器能力 chip 上,那才是回报它们的表面。 + Goal 通道继续署名该 Goal 自己的 Agent。 +* **Runtime 选择按通道的解析方式解析。** 在管家上下文里,聊天 Runtime 选择器遵循通道 + 属主的优先级:先看本机声明的管家执行器,只有本机什么都没声明时才回落到出货默认值。 + 一个被发现的适配器永远不会被呈现成管家。 + +运维方的显式选择仍然在该上下文里优先;运维方没有选择时,客户端依旧不发送任何端点, +因此 `loopx/chat_server.py` 的建会话契约——"每个通道通过自己的属主解析自己的默认值" +——没有变化。显式选择也是这条通道离开已声明执行器的唯一路径,这让一个被发现的 CLI +不会静默改写一项机器决定。 + +证据:`examples/personal-workspace-browser-smoke.mjs`(`execution-chip` 场景覆盖选择器与 +输入框的解析,`chat-recovery` 场景覆盖回答身份),以及在已安装 Dashboard 上的一次现场 +回读:页头 chip 解析为 `dsh`,而选择器此前报的是 `Codex`。 + ## 管家团队入端口径(2026-09-16) 管家负责回答问题。自 2026-09-16 起,它的出厂指引里还带了一段有界流程,用于另一类请求: