Skip to content

Commit c088d63

Browse files
authored
Merge pull request #5009 from loopx-project/codex/fix-machine-capability-catalog-0924
fix(settings): restore machine capability catalog after editor kind drift
2 parents 0056a31 + 2ee12b9 commit c088d63

6 files changed

Lines changed: 74 additions & 3 deletions

File tree

‎apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useMemo, useState } from "react";
2-
import { Check, Code2, RotateCcw, ShieldCheck, Trash2 } from "lucide-react";
2+
import { AlertTriangle, Check, Code2, RefreshCw, RotateCcw, ShieldCheck, Trash2 } from "lucide-react";
33

44
import {
55
applyMachineConfiguration,
@@ -152,6 +152,19 @@ export function MachineConfigurationSettings({ section }: { section: "steward" |
152152
setInspection(await fetchMachineConfiguration());
153153
}
154154

155+
async function retryLoad() {
156+
if (busy) return;
157+
setBusy("load");
158+
setError(null);
159+
try {
160+
await reload();
161+
} catch (cause) {
162+
setError(cause instanceof Error ? cause.message : t("machine.loadError"));
163+
} finally {
164+
setBusy(null);
165+
}
166+
}
167+
155168
useEffect(() => {
156169
let active = true;
157170
fetchMachineConfiguration()
@@ -309,6 +322,13 @@ export function MachineConfigurationSettings({ section }: { section: "steward" |
309322
if (busy === "load") {
310323
return <div className="personal-machine-loading" role="status">{t("common.loading")}</div>;
311324
}
325+
if (!inspection) {
326+
return <section className="personal-capability-error" role="alert">
327+
<AlertTriangle aria-hidden size={18} />
328+
<span><strong>{t("machine.loadError")}</strong><small>{error}</small></span>
329+
<button onClick={() => void retryLoad()} type="button"><RefreshCw aria-hidden size={15} />{t("capabilities.retry")}</button>
330+
</section>;
331+
}
312332
if (!selected) {
313333
return <p className="personal-capability-empty">{t("machine.capabilityEmpty")}</p>;
314334
}

‎examples/personal-workspace-browser/fixture.mjs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
425425
goalConfigurationRequests: [],
426426
machineConfigurationRequests: [],
427427
machineInspectionStatus: "configured",
428+
failNextMachineInspection: false,
428429
invalidMachineNamespaces: [],
429430
larkWrites: [],
430431
actionTransitions: [],
@@ -1067,6 +1068,11 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
10671068
},
10681069
};
10691070
if (url.pathname === "/api/chat/machine-configuration" && request.method() === "GET") {
1071+
if (state.failNextMachineInspection) {
1072+
state.failNextMachineInspection = false;
1073+
await route.fulfill({ contentType: "application/json", json: { error: "Machine catalog temporarily unavailable" }, status: 503 });
1074+
return;
1075+
}
10701076
await route.fulfill({ contentType: "application/json", json: {
10711077
...machineConfigurationBase,
10721078
schema_version: "machine_configuration_inspection_v0",

‎examples/personal-workspace-browser/typed-actions.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,8 +1128,15 @@ export const typedActionsScenario = {
11281128
if (providerOverlap) throw new Error(`Model provider category ${providerOverlap}`);
11291129
await page.screenshot({ path: resolve(outputDir, "model-provider-settings-zh-cn.png"), fullPage: false, animations: "disabled" });
11301130

1131+
api.failNextMachineInspection = true;
11311132
await page.getByRole("button", { name: "能力中心", exact: true }).click();
11321133
await page.getByRole("heading", { level: 1, name: "能力中心", exact: true }).waitFor({ state: "visible" });
1134+
const loadError = page.getByRole("alert").filter({ hasText: "无法读取机器配置" });
1135+
await loadError.waitFor({ state: "visible" });
1136+
if (await page.getByText("当前没有注册可在机器作用域配置的能力。", { exact: true }).count()) {
1137+
throw new Error("A failed machine catalog request was presented as an empty registry");
1138+
}
1139+
await loadError.getByRole("button", { name: "重试" }).click();
11331140
// The catalog workbench mounts after its inspection resolves, so the
11341141
// category's contents are asserted only once the workbench itself exists.
11351142
await page.locator(".personal-capability-layout").waitFor({ state: "visible" });

‎loopx/capabilities/configuration_ui.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def capability_configuration_editor(
324324
_field(
325325
"drift_threshold",
326326
"Consecutive drift receipts before an obligation",
327-
"integer",
327+
"number",
328328
minimum=PROGRESS_REVIEW_MIN_DRIFT_THRESHOLD,
329329
maximum=PROGRESS_REVIEW_MAX_DRIFT_THRESHOLD,
330330
),

‎skills/loopx-self-repair/references/repair-patterns.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.
55

66
| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
77
| --- | --- | --- | --- | --- |
8+
| `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. |
89
| `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. |
910
| `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. |
1011
| `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. |

‎tests/capabilities/test_capability_configuration_ui.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,44 @@
77
capability_configuration_editor,
88
resolve_capability_configuration,
99
)
10-
from loopx.configuration_catalog import build_goal_configuration_catalog
10+
from loopx.capabilities.machine_configuration.builtins import (
11+
build_builtin_machine_configuration_registry,
12+
)
13+
from loopx.configuration_catalog import (
14+
build_configuration_capability_descriptors,
15+
build_goal_configuration_catalog,
16+
)
17+
18+
19+
def test_machine_catalog_only_uses_dashboard_supported_editor_kinds() -> None:
20+
"""A Goal-only descriptor must not make the whole machine page unreadable."""
21+
22+
catalog = build_capability_configuration_catalog(
23+
machine_namespaces=build_builtin_machine_configuration_registry().public_catalog()[
24+
"namespaces"
25+
],
26+
goal_features=build_configuration_capability_descriptors(),
27+
)
28+
supported = {
29+
"boolean",
30+
"number",
31+
"select",
32+
"string_list",
33+
"text",
34+
"periodic_report_schedule",
35+
}
36+
fields = {
37+
(capability["capability_id"], field["key"]): field
38+
for capability in catalog["capabilities"]
39+
for field in capability["configuration_editor"]["fields"]
40+
}
41+
assert fields[("progress_review", "drift_threshold")]["input_kind"] == "number"
42+
unsupported = {
43+
key: field["input_kind"]
44+
for key, field in fields.items()
45+
if field["input_kind"] not in supported
46+
}
47+
assert unsupported == {}
1148

1249

1350
def test_periodic_report_editor_is_shared_across_machine_and_goal_scopes() -> None:

0 commit comments

Comments
 (0)