fix(claude): require current setup metadata and recover failed reads

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 19:17:23 -05:00
1 parent d663c7f98f
commit 193d8e188a
4 files changed
+111 -9

No files matched your search

@@ -555,3 +555,39 @@ The retained installed preview has been inspected in light/dark mode with the
original theme restored and agent Save disabled. The browser's requested mobile
override did not affect its actual 1150-pixel viewport, so that attempt is not
mobile proof and its override was reset. Final responsive qualification remains.
## Warm metadata, dependency recovery and owned startup — 2026-10-12
Three warm-cache regressions reproduced configuration construction while required
metadata was unresolved. Onboarding now waits for current environment, settings
and policy reads as well as matching runner discovery, including refetch over
cached successful data. Each prerequisite failure is visible and Retry setup
refetches the required reads without testing or hiring early. This also closes
fresh review 4253377849. The neutral-role step fixture's empty discovery response
reproduced the exact cloud failure; it now supplies valid Legacy discovery while
retaining role/name assertions. All 193 wizard/step/guided-setup/builder controls,
UI noemit, maps and token gates pass. Exploratory guided-connection changes were
removed; its existing connection flow remains intact.
Cloud platform job 115058187599 exposed ENOTEMPTY during late-bootstrap cleanup.
A deterministic held-writer test proves transport close returned before owned
credential preparation settled. A separate production-probe test proves the
helper called cleanup-confirmed during that unconfirmed preparation. These are
actual ownership defects, not a waived filesystem retry. Protocol repair
`463ebb62661d81ef1fbd79ae0a7b43ece68830f8` tracks startup, joins it within the
existing close budget and blocks later activation/launch. A drain timeout retains
state; late registration retires only its owned route. Primary setup timeout and
unconfirmed cleanup both remain in the failure. Six no-provider controls pass,
including writer drain, bounded retention, late release/error and the actual
probe callback fence. Runner noemit, declaration generation and maps pass; no
provider process, local Rust or Docker build was used. Historical Pi/Copilot
identities are unchanged by this transport-only correction.
The repair is in this chat's protocol worktree, not the frozen upstream #16028.
Its placement must be reconciled with the source owner's normal merge sequence
before either default rollout merges. Automatic approval review rejected cross-
chat status messages because it did not accept the earlier approval retrieved
from the source chat as current-chat authorization; the direct coordination
question remains pending. Public PR/evidence updates continue within this task.
Fresh exact-head cloud runner/native execution is required, including the failing
late-bootstrap case. No package campaign or new paid task has been dispatched.
@@ -178,7 +178,10 @@ describe("OnboardingWizard — which step it lands on", () => {
dialogState.onboardingOpen = false;
dialogState.onboardingOptions = {};
dialogState.onboardingRouteDismissed = false;
mockAdaptersApi.list.mockResolvedValue([]);
// The scoped discovery contract must resolve before the hire seam runs.
mockAdaptersApi.list.mockResolvedValue(["claude_local", "codex_local"].map(type => ({
type, loaded: true, disabled: false, defaultRunner: "legacy", supportedRunners: ["legacy"],
})));
mockGoalsApi.list.mockResolvedValue([]);
mockAgentsApi.adapterModels.mockResolvedValue([]);
// The hire step lists the company's agents first so it can adopt one that
@@ -3170,6 +3170,57 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
} finally { await act(async () => root.unmount()); }
});
it.each(["environments", "settings", "policy"] as const)("does not use warm Claude discovery before %s resolves", async pending => {
let finish!: (value: any) => void;
const delayed = new Promise<any>(resolve => { finish = resolve; });
if (pending === "environments") mockEnvironmentsApi.list.mockReturnValue(delayed);
else if (pending === "settings") mockInstanceSettingsApi.get.mockReturnValue(delayed);
else mockInstanceSettingsApi.getExperimental.mockReturnValue(delayed);
mockAdapterBuild.buildAdapterConfig.mockImplementation(values => buildClaudeLocalConfig(values!));
const { root, queryClient } = await openStep4({ adapterType: "claude_local" });
try {
await act(async () => {
// A warm value remains successful while its current fetch is pending.
if (pending === "environments") queryClient.setQueryData(queryKeys.environments.list("company-new"), [LOCAL_ENVIRONMENT]);
else if (pending === "settings") queryClient.setQueryData(queryKeys.instance.settings, { defaultEnvironmentId: null });
else queryClient.setQueryData(queryKeys.instance.experimentalSettings, { enableManagedSandboxOnly: false });
for (const environmentId of [null, LOCAL_ENVIRONMENT.id]) queryClient.setQueryData(queryKeys.adapters.availability("company-new", environmentId), [{ type: "claude_local", defaultRunner: "paperclip" }]);
queryClient.setQueryData(["ai-connections", "company-new"], { currentUserId: "user-1", connections: [{ id: "saved", grantId: "grant", companyId: "company-new", provider: "anthropic", method: "subscription", ownership: "personal", ownerUserId: "user-1", status: "connected", isDefault: true }] });
});
await pickSource(/Claude/);
await settle();
expect(mockAdapterBuild.buildAdapterConfig).not.toHaveBeenCalled();
expect(mockAgentsApi.testEnvironment).not.toHaveBeenCalled();
expect(mockAgentsApi.hire).not.toHaveBeenCalled();
await act(async () => finish(pending === "environments" ? [LOCAL_ENVIRONMENT] : pending === "settings" ? { defaultEnvironmentId: null } : { enableManagedSandboxOnly: false }));
await settle();
expect(mockAgentsApi.hire).toHaveBeenCalledTimes(1);
const hired = mockAgentsApi.hire.mock.calls[0] as unknown as [string, { adapterConfig: Record<string, unknown> }];
expect(hired[1].adapterConfig.maxTurnsPerRun).toBe(1000);
} finally { await act(async () => root.unmount()); }
});
it.each(["environments", "settings", "policy"] as const)("reports failed Claude %s resolution and retries without early verification", async failed => {
if (failed === "environments") mockEnvironmentsApi.list.mockRejectedValue(new Error("Unavailable"));
else if (failed === "settings") mockInstanceSettingsApi.get.mockRejectedValue(new Error("Unavailable"));
else mockInstanceSettingsApi.getExperimental.mockRejectedValue(new Error("Unavailable"));
const { root, queryClient } = await openStep4({ adapterType: "claude_local" });
try {
await act(async () => queryClient.setQueryData(["ai-connections", "company-new"], { currentUserId: "user-1", connections: [{ id: "saved", grantId: "grant", companyId: "company-new", provider: "anthropic", method: "subscription", ownership: "personal", ownerUserId: "user-1", status: "connected", isDefault: true }] }));
await pickSource(/Claude/);
await settle();
expect(document.body.textContent).toContain("Could not load execution environment settings");
expect(mockAgentsApi.testEnvironment).not.toHaveBeenCalled();
expect(mockAgentsApi.hire).not.toHaveBeenCalled();
mockEnvironmentsApi.list.mockResolvedValue([LOCAL_ENVIRONMENT]);
mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: null });
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: false });
await click("Retry setup");
expect(mockAgentsApi.testEnvironment).toHaveBeenCalledTimes(1);
expect(mockAgentsApi.hire).toHaveBeenCalledTimes(1);
} finally { await act(async () => root.unmount()); }
});
it("does not hire from obsolete Claude discovery after the target changes during verification", async () => {
mockAdapterBuild.buildAdapterConfig.mockImplementation(values => buildClaudeLocalConfig(values!));
mockAdaptersApi.list.mockImplementation(async ({ environmentId }: { environmentId: string | null }) => [{ type: "claude_local", defaultRunner: environmentId === "new-local-default" ? "paperclip" : "legacy" }]);
+20 -8
View File
@@ -947,14 +947,14 @@ function OnboardingWizardInner({
// managed-sandbox-only redirect (see AgentConfigForm.tsx:618-640). A render
// must not throw, so a resolver error yields no login environment rather
// than an error boundary.
const { data: loginEnvironmentList = [], isSuccess: loginEnvironmentsReady } = useQuery({
const { data: loginEnvironmentList = [], isSuccess: loginEnvironmentsReady, isFetching: loginEnvironmentsFetching, isError: loginEnvironmentsFailed, refetch: refetchLoginEnvironments } = useQuery({
queryKey: createdCompanyId
? queryKeys.environments.list(createdCompanyId)
: ["environments", "none"],
queryFn: () => environmentsApi.list(createdCompanyId!),
enabled: Boolean(createdCompanyId) && effectiveOnboardingOpen && step === 4,
});
const { data: instanceSettingsForLogin, isSuccess: loginSettingsReady } = useQuery({
const { data: instanceSettingsForLogin, isSuccess: loginSettingsReady, isFetching: loginSettingsFetching, isError: loginSettingsFailed, refetch: refetchLoginSettings } = useQuery({
queryKey: queryKeys.instance.settings,
queryFn: () => instanceSettingsApi.get(),
enabled: effectiveOnboardingOpen && step === 4,
@@ -962,7 +962,7 @@ function OnboardingWizardInner({
// Wanted across the whole arc, not just the connect step. The progress strip
// reads it too — see `enteredFromCloud` — and a value fetched only on step 4
// would let the strip change length as the customer walked through it.
const { data: experimentalSettingsForLogin, isSuccess: loginPolicyReady } = useQuery({
const { data: experimentalSettingsForLogin, isSuccess: loginPolicyReady, isFetching: loginPolicyFetching, isError: loginPolicyFailed, refetch: refetchLoginPolicy } = useQuery({
queryKey: queryKeys.instance.experimentalSettings,
queryFn: () => instanceSettingsApi.getExperimental(),
enabled: effectiveOnboardingOpen && step >= 3 && step <= 5,
@@ -985,23 +985,35 @@ function OnboardingWizardInner({
loginEnvironmentList,
experimentalSettingsForLogin?.enableManagedSandboxOnly,
]);
const loginEnvironmentSettingsReady = loginEnvironmentsReady && loginSettingsReady && loginPolicyReady
&& !loginEnvironmentsFetching && !loginSettingsFetching && !loginPolicyFetching;
const runnerDiscovery = useQuery({
queryKey: queryKeys.adapters.availability(createdCompanyId, resolvedLoginEnvironmentId),
queryFn: () => adaptersApi.list({ companyId: createdCompanyId!, environmentId: resolvedLoginEnvironmentId }),
enabled: Boolean(createdCompanyId) && effectiveOnboardingOpen && step === 4
&& loginEnvironmentsReady && loginSettingsReady && loginPolicyReady,
&& loginEnvironmentSettingsReady,
});
const needsRunnerDiscovery = ["codex_local", "claude_local"].includes(adapterType) && runner === "auto";
const resolvedRunner = runner === "auto"
? runnerDiscovery.data?.find(item => item.type === adapterType)?.defaultRunner
: runner;
const runnerDiscoveryReady = !needsRunnerDiscovery
|| (runnerDiscovery.isSuccess && !runnerDiscovery.isFetching && resolvedRunner !== undefined);
const runnerDiscoveryError = needsRunnerDiscovery && runnerDiscovery.isError
? "Could not load runner availability. Retry setup."
|| (loginEnvironmentSettingsReady
&& runnerDiscovery.isSuccess && !runnerDiscovery.isFetching && resolvedRunner !== undefined);
const runnerDiscoveryError = needsRunnerDiscovery && (loginEnvironmentsFailed || loginSettingsFailed || loginPolicyFailed)
? "Could not load execution environment settings. Retry setup."
: needsRunnerDiscovery && runnerDiscovery.isError
? "Could not load runner availability. Retry setup."
: needsRunnerDiscovery && runnerDiscovery.isSuccess && resolvedRunner === undefined
? "Runner availability is missing for this harness. Retry setup."
: null;
async function retryRunnerAvailability() {
if (!loginEnvironmentSettingsReady) {
await Promise.all([refetchLoginEnvironments(), refetchLoginSettings(), refetchLoginPolicy()]);
} else {
await runnerDiscovery.refetch();
}
}
const visibleAdapterEnvError = adapterEnvError ?? runnerDiscoveryError;
const runnerSelectionKey = JSON.stringify([createdCompanyId, resolvedLoginEnvironmentId, adapterType, runner, resolvedRunner]);
const runnerSelectionRef = useRef(runnerSelectionKey);
@@ -2966,7 +2978,7 @@ function OnboardingWizardInner({
)}
{runnerDiscoveryError && (
<Button type="button" variant="outline" disabled={runnerDiscovery.isFetching} onClick={() => void runnerDiscovery.refetch()}>
<Button type="button" variant="outline" disabled={runnerDiscovery.isFetching || loginEnvironmentsFetching || loginSettingsFetching || loginPolicyFetching} onClick={() => void retryRunnerAvailability()}>
Retry setup
</Button>
)}