From 73185c63fe59518e6a166e735ba9bee68360f6c9 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Mon, 26 Oct 2026 09:58:53 -0700 Subject: [PATCH] Recover setup after environment and policy changes Resume preparation after a stop completes or a policy hold clears. Keep probes blocked when the selected environment could not be saved, preserve the account, and offer a save retry. Verified 64 lifecycle/policy tests, the focused policy recovery case, 110 wizard/helper tests, and server/UI TypeScript checks. Co-Authored-By: Paperclip --- .../__tests__/lifecycle.test.ts | 9 +++++-- .../agent-lifecycle/adapters/postgres.ts | 2 ++ ui/src/components/new-agent/NewAgentSetup.tsx | 24 ++++++++++------- ui/src/pages/NewAgent.test.tsx | 26 +++++++++++++++++++ 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts b/server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts index 047c50a872a..48dd67cf95b 100644 --- a/server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts +++ b/server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts @@ -171,8 +171,8 @@ const support = await getEmbeddedPostgresTestSupport(); expect(await current(id)).toMatchObject({ lifecycleState: "paused", lifecycleHolds: ["setup", "budget"] }); await lifecycle.updateConfiguration(id, { adapterConfig: { provider: "codex", model: "changed" } }); expect((await current(id)).lifecycleState).toBe("paused"); - // Restore only the setup hold to exercise environment changes under the wizard's authority. - await db.update(agents).set({ lifecycleHolds: ["setup"], pauseReason: "setup" }).where(eq(agents.id, id)); + await store.change(id, "reconcile"); await work.process(id); + expect(await current(id)).toMatchObject({ lifecycleState: "verifying", lifecycleHolds: ["setup"] }); await lifecycle.updateConfiguration(id, { adapterConfig: { provider: "codex", model: "prepared" } }); await work.process(id); expect((await current(id)).lifecycleState).toBe("verifying"); @@ -181,6 +181,11 @@ const support = await getEmbeddedPostgresTestSupport(); expect((await current(id)).lifecycleState).toBe("paused"); await lifecycle.updateConfiguration(id, { defaultEnvironmentId: environmentId }); await work.process(id); expect((await current(id)).lifecycleState).toBe("verifying"); + await lifecycle.updateConfiguration(id, { defaultEnvironmentId: local.id }); + expect((await current(id)).lifecycleState).toBe("pausing"); + await lifecycle.updateConfiguration(id, { defaultEnvironmentId: environmentId }); + await work.process(id); + expect((await current(id)).lifecycleState).toBe("verifying"); const finished = await lifecycle.requestHire(companyId, input, { setupAgentId: id, runnerResolved: true }); expect(finished.id).toBe(id); await work.process(id); diff --git a/server/src/modules/agent-lifecycle/adapters/postgres.ts b/server/src/modules/agent-lifecycle/adapters/postgres.ts index 3e9a50a3c45..4179fa923aa 100644 --- a/server/src/modules/agent-lifecycle/adapters/postgres.ts +++ b/server/src/modules/agent-lifecycle/adapters/postgres.ts @@ -62,6 +62,8 @@ async function changeLockedAgent(tx: Db, effects: LifecycleEffects, agent: Lifec if (restartPause && next === "paused") next = "pausing"; if (command === "complete" && next === "ready" && holds.length) next = "pausing"; if (command === "complete" && next === "paused" && !holds.length) next = "resuming"; + if (["complete", "reconcile"].includes(command) && next === "paused" && holds.length === 1 && holds[0] === "setup" + && await effects.setupRequiresPreparation(tx, agent.companyId, agent.defaultEnvironmentId)) next = "preparing"; if (next === agent.lifecycleState) { if (JSON.stringify(holds) !== JSON.stringify(agent.lifecycleHolds) || agent.pauseReason !== (holds[0] ?? null)) { const [updated] = await tx.update(agents).set({ lifecycleHolds: holds, pauseReason: holds[0] ?? null }).where(eq(agents.id, agent.id)).returning(); diff --git a/ui/src/components/new-agent/NewAgentSetup.tsx b/ui/src/components/new-agent/NewAgentSetup.tsx index 9bb13435f46..429c7c59730 100644 --- a/ui/src/components/new-agent/NewAgentSetup.tsx +++ b/ui/src/components/new-agent/NewAgentSetup.tsx @@ -413,7 +413,9 @@ function Setup({ ); const needsCurrentSetup = ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType); const preparingComputer = created?.pauseReason === "setup" && ["preparing", "pausing"].includes(created.lifecycleState ?? ""); - const ready = Boolean(created) && !environmentSaving && !preparingComputer && connectionSettingsReady && (!needsCurrentSetup || ( + const environmentSelectionSaved = (created?.defaultEnvironmentId ?? null) + === (environmentOverride || (forced.forced || managedOnly ? environmentId : null)); + const ready = Boolean(created) && environmentSelectionSaved && !environmentSaving && !preparingComputer && connectionSettingsReady && (!needsCurrentSetup || ( envs.isSuccess && !envs.isFetching && settings.isSuccess && !settings.isFetching && experimental.isSuccess && !experimental.isFetching @@ -657,6 +659,15 @@ function Setup({ finally { savingRef.current = false; setSaving(false); } } + async function changeEnvironment(selected = environmentOverride) { + setEnvironmentOverride(selected); + resetTest(); + setEnvironmentSaving(true); + try { await saveDraft(connection, false, undefined, selected); } + catch (cause) { setError(cause instanceof Error ? cause.message : "Could not save the environment."); } + finally { setEnvironmentSaving(false); } + } + async function runTest(nextConnection = connection): Promise | null> { if (!ready) return null; const run = ++generation.current; @@ -1004,7 +1015,7 @@ function Setup({ ) ?? result?.checks.find( (check) => - check.code.includes("hello_probe") && + (check.code.includes("hello_probe") || check.code.startsWith("environment_agent_")) && check.level === "warn", ) )?.message @@ -1390,13 +1401,7 @@ function Setup({ value={environmentOverride} disabled={forced.forced} onChange={(event) => { - const selected = event.target.value; - setEnvironmentOverride(selected); - resetTest(); - setEnvironmentSaving(true); - void saveDraft(connection, false, undefined, selected) - .catch(cause => setError(cause instanceof Error ? cause.message : "Could not save the environment.")) - .finally(() => setEnvironmentSaving(false)); + void changeEnvironment(event.target.value); }} >