Make interrupted-agent recovery opt-in (#8619)

* fix(agents): require opt-in for restart recovery

Keep automatic interrupted-turn resumption off during further dogfooding. Preserve explicit opt-in and custom resume prompts.

* test(settings): verify restart recovery opt-in persists

* docs(agents): describe restart recovery opt-in
This commit is contained in:
Mohamed Boudra
2026-11-03 10:20:26 +07:00
committed by GitHub
parent a296bf3502
commit 6f6712be62
7 changed files with 36 additions and 14 deletions
@@ -262,12 +262,15 @@ test.describe("Settings host page", () => {
});
});
test("restart recovery host controls persist the opt-out and custom prompt", async ({ page }) => {
test("restart recovery defaults off and persists opt-in and custom prompt", async ({ page }) => {
const serverId = getServerId();
await gotoAppShell(page);
await openSettings(page);
await openSettingsHost(page, serverId);
await openHostSection(page, serverId, "agents");
await expectRestartRecoveryEnabled(page, false);
await setRestartRecoveryEnabled(page, true);
await page.reload();
await expectRestartRecoveryEnabled(page, true);
await expectRestartRecoveryPrompt(page, "Resume working.");
await editRestartRecoveryPrompt(page, "Continue the interrupted work.");
@@ -59,7 +59,7 @@ export function RestartRecoveryCard({ serverId }: { serverId: string }) {
if (!isConnected || !isSupported || !config) return null;
const isEnabled = config.resumeOnStartup !== false;
const isEnabled = config.resumeOnStartup === true;
return (
<>
+2 -2
View File
@@ -600,7 +600,7 @@ function createInitialMutableDaemonConfig(config: ResolvedDaemonSettings): Mutab
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
enableTerminalAgentHooks: config.enableTerminalAgentHooks ?? false,
idleRuntimeCleanup: config.idleRuntimeCleanup !== false,
resumeOnStartup: config.resumeOnStartup !== false,
resumeOnStartup: config.resumeOnStartup === true,
resumePrompt: config.resumePrompt ?? "Resume working.",
appendSystemPrompt: config.appendSystemPrompt ?? "",
pluginsEnabled: config.pluginsEnabled ?? false,
@@ -1047,7 +1047,7 @@ export async function createPaseoDaemon(
const restartRecovery = createRestartRecovery({
storage: agentStorage,
settings: () => ({
enabled: daemonConfigStore.get().resumeOnStartup !== false,
enabled: daemonConfigStore.get().resumeOnStartup === true,
prompt: daemonConfigStore.get().resumePrompt ?? "Resume working.",
}),
eligible: async (record) =>
+18
View File
@@ -40,6 +40,24 @@ describe("server config", () => {
expect(blockingMs).toBeLessThan(50);
});
test.each([undefined, false, true])(
"restart recovery requires explicit opt-in (%s)",
async (enabled) => {
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-config-recovery-"));
roots.push(paseoHome);
await writeFile(
path.join(paseoHome, "config.json"),
JSON.stringify({
version: 1,
agents: { resumeOnStartup: enabled, resumePrompt: "Continue exactly.\n" },
}),
);
const config = loadConfig(paseoHome, { env: {} });
expect(config.resumeOnStartup).toBe(enabled === true);
expect(config.resumePrompt).toBe("Continue exactly.\n");
},
);
test("records when the daemon is managed by Paseo Desktop", async () => {
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-config-desktop-managed-"));
roots.push(paseoHome);
+1 -1
View File
@@ -839,7 +839,7 @@ function resolveSpeechOverrideControlledPaths(
function resolveRestartRecoveryConfig(agents: PersistedConfig["agents"]) {
return {
resumeOnStartup: agents?.resumeOnStartup ?? true,
resumeOnStartup: agents?.resumeOnStartup ?? false,
resumePrompt: agents?.resumePrompt ?? "Resume working.",
};
}
@@ -134,7 +134,7 @@ test.each([false, true])(
let client: DaemonClient | undefined;
let mcp: Client | undefined;
try {
const first = await restartFixture(root);
const first = await restartFixture(root, { resumeOnStartup: true });
processes.push(first.child);
client = new DaemonClient({ url: `ws://127.0.0.1:${first.port}/ws`, appVersion: "0.13.0" });
await client.connect();
@@ -182,7 +182,7 @@ test.each([false, true])(
await client.close();
client = undefined;
await stopFixture(first.child, abrupt);
const second = await restartFixture(root);
const second = await restartFixture(root, { resumeOnStartup: true });
processes.push(second.child);
await expect.poll(() => readPrompts(root), { timeout: 15000 }).toHaveLength(1);
const prompts = await readPrompts(root);
@@ -222,7 +222,7 @@ test.each([false, true])(
await client.close();
client = undefined;
await stopFixture(second.child, false);
const third = await restartFixture(root);
const third = await restartFixture(root, { resumeOnStartup: true });
processes.push(third.child);
expect(await readPrompts(root)).toHaveLength(1);
expect(
@@ -243,7 +243,7 @@ test.each([false, true])(
60000,
);
test.each([false, true])(
test.each([undefined, false, true])(
"host restart recovery settings (enabled=%s)",
async (enabled) => {
const root = await mkdtemp(path.join(tmpdir(), "resume-settings-process-"));
@@ -314,7 +314,7 @@ test("a provider that is slow to reopen does not hold daemon startup", async ()
const processes: ChildProcess[] = [];
let client: DaemonClient | undefined;
try {
const first = await restartFixture(root);
const first = await restartFixture(root, { resumeOnStartup: true });
processes.push(first.child);
client = new DaemonClient({ url: `ws://127.0.0.1:${first.port}/ws`, appVersion: "0.13.0" });
await client.connect();
@@ -338,7 +338,7 @@ test("a provider that is slow to reopen does not hold daemon startup", async ()
client = undefined;
await stopFixture(first.child, false);
const second = await restartFixture(root, {}, { holdResume: true });
const second = await restartFixture(root, { resumeOnStartup: true }, { holdResume: true });
processes.push(second.child);
client = new DaemonClient({ url: `ws://127.0.0.1:${second.port}/ws`, appVersion: "0.13.0" });
await client.connect();