From 7bb59824bb0b84502a83ee04259c7f9e2620c206 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:52:00 +0800 Subject: [PATCH 1/2] fix(capabilities): keep progress review editor within catalog vocabulary Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- loopx/capabilities/configuration_ui.py | 2 +- .../test_capability_configuration_ui.py | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py index ff1550c13d..0c5af03015 100644 --- a/loopx/capabilities/configuration_ui.py +++ b/loopx/capabilities/configuration_ui.py @@ -324,7 +324,7 @@ def capability_configuration_editor( _field( "drift_threshold", "Consecutive drift receipts before an obligation", - "integer", + "number", minimum=PROGRESS_REVIEW_MIN_DRIFT_THRESHOLD, maximum=PROGRESS_REVIEW_MAX_DRIFT_THRESHOLD, ), diff --git a/tests/capabilities/test_capability_configuration_ui.py b/tests/capabilities/test_capability_configuration_ui.py index 92955a0787..c708badd4b 100644 --- a/tests/capabilities/test_capability_configuration_ui.py +++ b/tests/capabilities/test_capability_configuration_ui.py @@ -7,7 +7,44 @@ capability_configuration_editor, resolve_capability_configuration, ) -from loopx.configuration_catalog import build_goal_configuration_catalog +from loopx.capabilities.machine_configuration.builtins import ( + build_builtin_machine_configuration_registry, +) +from loopx.configuration_catalog import ( + build_configuration_capability_descriptors, + build_goal_configuration_catalog, +) + + +def test_machine_catalog_only_uses_dashboard_supported_editor_kinds() -> None: + """A Goal-only descriptor must not make the whole machine page unreadable.""" + + catalog = build_capability_configuration_catalog( + machine_namespaces=build_builtin_machine_configuration_registry().public_catalog()[ + "namespaces" + ], + goal_features=build_configuration_capability_descriptors(), + ) + supported = { + "boolean", + "number", + "select", + "string_list", + "text", + "periodic_report_schedule", + } + fields = { + (capability["capability_id"], field["key"]): field + for capability in catalog["capabilities"] + for field in capability["configuration_editor"]["fields"] + } + assert fields[("progress_review", "drift_threshold")]["input_kind"] == "number" + unsupported = { + key: field["input_kind"] + for key, field in fields.items() + if field["input_kind"] not in supported + } + assert unsupported == {} def test_periodic_report_editor_is_shared_across_machine_and_goal_scopes() -> None: From 2ee12b908309063873fef53c9c0abdb814c58abe Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:52:32 +0800 Subject: [PATCH 2/2] fix(dashboard): show machine catalog load failures and retry Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../machine-configuration-settings.tsx | 22 ++++++++++++++++++- .../personal-workspace-browser/fixture.mjs | 6 +++++ .../typed-actions.mjs | 7 ++++++ .../references/repair-patterns.md | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx index 78e6a5d7fe..461f184c82 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { Check, Code2, RotateCcw, ShieldCheck, Trash2 } from "lucide-react"; +import { AlertTriangle, Check, Code2, RefreshCw, RotateCcw, ShieldCheck, Trash2 } from "lucide-react"; import { applyMachineConfiguration, @@ -152,6 +152,19 @@ export function MachineConfigurationSettings({ section }: { section: "steward" | setInspection(await fetchMachineConfiguration()); } + async function retryLoad() { + if (busy) return; + setBusy("load"); + setError(null); + try { + await reload(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : t("machine.loadError")); + } finally { + setBusy(null); + } + } + useEffect(() => { let active = true; fetchMachineConfiguration() @@ -309,6 +322,13 @@ export function MachineConfigurationSettings({ section }: { section: "steward" | if (busy === "load") { return
{t("common.loading")}
; } + if (!inspection) { + return
+ + {t("machine.loadError")}{error} + +
; + } if (!selected) { return

{t("machine.capabilityEmpty")}

; } diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs index 3097c68190..61d2174aa5 100644 --- a/examples/personal-workspace-browser/fixture.mjs +++ b/examples/personal-workspace-browser/fixture.mjs @@ -425,6 +425,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true goalConfigurationRequests: [], machineConfigurationRequests: [], machineInspectionStatus: "configured", + failNextMachineInspection: false, invalidMachineNamespaces: [], larkWrites: [], actionTransitions: [], @@ -1067,6 +1068,11 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true }, }; if (url.pathname === "/api/chat/machine-configuration" && request.method() === "GET") { + if (state.failNextMachineInspection) { + state.failNextMachineInspection = false; + await route.fulfill({ contentType: "application/json", json: { error: "Machine catalog temporarily unavailable" }, status: 503 }); + return; + } await route.fulfill({ contentType: "application/json", json: { ...machineConfigurationBase, schema_version: "machine_configuration_inspection_v0", diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 19a501c2f5..48c1a8d4c1 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -1128,8 +1128,15 @@ export const typedActionsScenario = { if (providerOverlap) throw new Error(`Model provider category ${providerOverlap}`); await page.screenshot({ path: resolve(outputDir, "model-provider-settings-zh-cn.png"), fullPage: false, animations: "disabled" }); + api.failNextMachineInspection = true; await page.getByRole("button", { name: "能力中心", exact: true }).click(); await page.getByRole("heading", { level: 1, name: "能力中心", exact: true }).waitFor({ state: "visible" }); + const loadError = page.getByRole("alert").filter({ hasText: "无法读取机器配置" }); + await loadError.waitFor({ state: "visible" }); + if (await page.getByText("当前没有注册可在机器作用域配置的能力。", { exact: true }).count()) { + throw new Error("A failed machine catalog request was presented as an empty registry"); + } + await loadError.getByRole("button", { name: "重试" }).click(); // The catalog workbench mounts after its inspection resolves, so the // category's contents are asserted only once the workbench itself exists. await page.locator(".personal-capability-layout").waitFor({ state: "visible" }); diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index fed3e1476d..1a23211349 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | +| `capability_catalog_editor_kind_drift` | Machine or Goal settings report an empty capability list even though the configuration API returns registered capabilities. | Live API catalog IDs and editor kinds, the dashboard's accepted field-kind schema, and the page's load-error state. | One new descriptor emits an unsupported field kind; strict validation rejects the shared catalog and the machine page presents the failed load as an empty registry. | Keep the published editor vocabulary aligned with the browser contract, check every built-in descriptor together, and show a retryable error when catalog loading or validation fails. Only a successfully loaded empty catalog may show the empty state. | | `acceptance_scope_capture` | A bounded validation experiment leaves unrelated existing/new work unbound; a recorded blocker quiets replan without repairing admission. | Canonical contract scope/bindings, exact held generation, authorized configuration source, ordinary task validators and post-correction claim/lease readback. | Omitted scope silently imposed Goal-wide acceptance; repeated per-task binding masked the missing scope contract. | Require explicit scope on new owner configuration, preserve legacy persisted semantics/replay, and enforce one typed scope across admission, completion and verification freshness. Expose scope on existing read surfaces. Diagnose scope before proposing rebinding; a blocker ACK is neither a repair nor a handoff. Apply authorized corrections through CAS and validate independent work resumes while selected holds and ordinary validation remain. | | `acceptance_hold_recovery_selection_split` | Newly created advancement work is acceptance-unbound, repeated vision replans never expose its hold, or a replan packet also selects an unrelated due monitor. | Canonical acceptance tasks, scoped source Todos, bounded trigger checkpoints, effective action and original Turn receipt. | Recovery covered stale associations only; generic vision gaps displaced hold identities; candidate inventory leaked into the selected execution target. | Route missing and stale associations through the existing bounded replan lane, retain exact hold checkpoints before generic gaps, and separate replan from candidate selection. Keep owner association and completion validation enforced. A new unbound repair Todo is not runnable recovery; only a qualified successor or concrete blocker settles the exact hold. Validate real File/SQLite CLI paths and receipt reentry without mutating an active Goal. | | `acceptance_validation_failure_flattened` | A bound Todo is `ready` and its ordinary validator passes, yet completion reports only `goal_acceptance_validation_rejected`; the agent searches contract bindings before discovering a workspace or runner failure. | Exact Todo acceptance state, completion's typed criterion failure, recorded delivery workspace, current worktree cleanliness, and the privacy-safe runner receipt. | The completion boundary collapsed a failed criterion receipt into a generic contract error, hiding the workspace or command status. | Project the failed criterion ID, allowlisted validation status, safe exit code and bounded next action without command output or local paths. Repair the execution context and retry under the same Turn/lease; do not rebind owner criteria or infer a Goal-wide hold. |