mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
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:
@@ -14,9 +14,10 @@ Each live agent in `AgentManager` carries a `lastStatus` of `initializing`, `idl
|
||||
|
||||
## Daemon restart recovery
|
||||
|
||||
Hosts resume interrupted agents by default after graceful shutdown or a crash. The daemon
|
||||
reopens the saved provider conversation and settings, then sends `Resume working.`. Change
|
||||
`agents.resumePrompt` or disable `agents.resumeOnStartup` in host config or Host settings → Agents.
|
||||
Restart recovery is off by default. Enable `agents.resumeOnStartup` in host config or
|
||||
Host settings → Agents to resume interrupted agents after graceful shutdown or a crash. The daemon
|
||||
reopens the saved provider conversation and settings, then sends `Resume working.`. Customize
|
||||
the prompt with `agents.resumePrompt` or the same settings page.
|
||||
Disabling recovery consumes pending intent; enabling it later does not revive old work.
|
||||
|
||||
Only agents recorded as running qualify. Idle and initializing agents stay inactive. Archived
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user