Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -309,6 +322,13 @@ export function MachineConfigurationSettings({ section }: { section: "steward" |
if (busy === "load") {
return <div className="personal-machine-loading" role="status">{t("common.loading")}</div>;
}
if (!inspection) {
return <section className="personal-capability-error" role="alert">
<AlertTriangle aria-hidden size={18} />
<span><strong>{t("machine.loadError")}</strong><small>{error}</small></span>
<button onClick={() => void retryLoad()} type="button"><RefreshCw aria-hidden size={15} />{t("capabilities.retry")}</button>
</section>;
}
if (!selected) {
return <p className="personal-capability-empty">{t("machine.capabilityEmpty")}</p>;
}
Expand Down
6 changes: 6 additions & 0 deletions examples/personal-workspace-browser/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
goalConfigurationRequests: [],
machineConfigurationRequests: [],
machineInspectionStatus: "configured",
failNextMachineInspection: false,
invalidMachineNamespaces: [],
larkWrites: [],
actionTransitions: [],
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions examples/personal-workspace-browser/typed-actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down
2 changes: 1 addition & 1 deletion loopx/capabilities/configuration_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
39 changes: 38 additions & 1 deletion tests/capabilities/test_capability_configuration_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading