mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 14:12:51 +00:00
fix(server): keep a legacy feature's recorded value when migrating it to a control
An agent stored with features and no controls showed its toggles as off and its selects as unset in a snapshot built while its runtime was unloaded, because only the default choice survived the migration. The recorded value is now the migrated control's unset value; an explicit effective option still wins.
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
} from "./agent-messages/index.js";
|
||||
import { expect, test, vi, onTestFinished } from "vitest";
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -13094,4 +13094,62 @@ describe("agent options", () => {
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("a metadata-only update of an agent stored with legacy features reports their values", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-legacy-features-"));
|
||||
const agentsDir = join(workdir, "agents");
|
||||
const first = new AgentManager({
|
||||
clients: { codex: new OptionsClient() },
|
||||
registry: new AgentStorage(agentsDir, logger),
|
||||
logger,
|
||||
});
|
||||
const created = await first.createAgent({ provider: "codex", cwd: workdir }, undefined, {
|
||||
workspaceId: "workspace-1",
|
||||
});
|
||||
await first.closeAgent(created.id);
|
||||
|
||||
// Rewrite the record as an older daemon stored it: features with values, no controls.
|
||||
const dir = readdirSync(agentsDir, { withFileTypes: true }).find(
|
||||
(entry) =>
|
||||
entry.isDirectory() && existsSync(join(agentsDir, entry.name, `${created.id}.json`)),
|
||||
)!;
|
||||
const file = join(agentsDir, dir.name, `${created.id}.json`);
|
||||
const stored = JSON.parse(readFileSync(file, "utf8"));
|
||||
delete stored.controls;
|
||||
stored.features = [
|
||||
{ type: "toggle", id: "fast_mode", label: "Fast", value: true },
|
||||
{
|
||||
type: "select",
|
||||
id: "voice",
|
||||
label: "Voice",
|
||||
value: "soft",
|
||||
options: [
|
||||
{ id: "direct", label: "Direct" },
|
||||
{ id: "soft", label: "Soft" },
|
||||
],
|
||||
},
|
||||
];
|
||||
writeFileSync(file, JSON.stringify(stored));
|
||||
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new OptionsClient() },
|
||||
registry: new AgentStorage(agentsDir, logger),
|
||||
logger,
|
||||
});
|
||||
const updates: ManagedAgent[] = [];
|
||||
manager.subscribe(
|
||||
(event) => {
|
||||
if (event.type === "agent_state" && event.agent.id === created.id)
|
||||
updates.push(event.agent);
|
||||
},
|
||||
{ replayState: false },
|
||||
);
|
||||
await manager.markAgentUnread(created.id);
|
||||
|
||||
const payload = toAgentPayload(updates.at(-1)!);
|
||||
expect(payload.features?.map((feature) => [feature.id, feature.value])).toEqual([
|
||||
["fast_mode", true],
|
||||
["voice", "soft"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,12 +252,12 @@ describe("AgentStorage", () => {
|
||||
expect(persisted?.config).not.toHaveProperty("featureValues");
|
||||
expect(persisted).not.toHaveProperty("features");
|
||||
expect(persisted?.controls).toEqual([
|
||||
{ type: "toggle", key: "fast_mode", label: "Fast mode" },
|
||||
{ type: "toggle", key: "fast_mode", label: "Fast mode", defaultValue: true },
|
||||
{
|
||||
type: "select",
|
||||
key: "service_tier",
|
||||
label: "Speed",
|
||||
defaultValue: "default",
|
||||
defaultValue: "fast",
|
||||
choices: [
|
||||
{ value: "default", label: "Normal" },
|
||||
{ value: "fast", label: "Fast" },
|
||||
|
||||
@@ -175,6 +175,10 @@ export function featuresFromControls(
|
||||
return (controls ?? []).map((control) => featureFromControl(control, effective));
|
||||
}
|
||||
|
||||
/**
|
||||
* A recorded feature's value is the control's unset value: it is what the agent showed before
|
||||
* any option was stored, and an explicit effective option still wins over it.
|
||||
*/
|
||||
function controlFromFeature(feature: AgentFeature): AgentOptionControl {
|
||||
const descriptor = {
|
||||
key: feature.id,
|
||||
@@ -182,14 +186,15 @@ function controlFromFeature(feature: AgentFeature): AgentOptionControl {
|
||||
...describe(feature),
|
||||
};
|
||||
if (feature.type === "toggle") {
|
||||
return { type: "toggle", ...descriptor };
|
||||
return { type: "toggle", ...descriptor, defaultValue: feature.value };
|
||||
}
|
||||
const defaultChoice = feature.options.find((option) => option.isDefault);
|
||||
const unsetValue = feature.value ?? defaultChoice?.id;
|
||||
return {
|
||||
type: "select",
|
||||
...descriptor,
|
||||
...(feature.desktopTrigger ? { desktopTrigger: feature.desktopTrigger } : {}),
|
||||
...(defaultChoice ? { defaultValue: defaultChoice.id } : {}),
|
||||
...(unsetValue != null ? { defaultValue: unsetValue } : {}),
|
||||
choices: feature.options.map((option) => ({
|
||||
value: option.id,
|
||||
label: option.label,
|
||||
|
||||
Reference in New Issue
Block a user