Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/cli/src/commands/doctor-opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "../lib/opencode-plugin-cache";
import { runV22BackfillCommands } from "../lib/v22-backfill-commands";
import {
checkUserMemoriesDreamerCompatibility,
collectNpmReleaseAgeWarnings,
getUserNpmrcPath,
isPinnedOpenCodePluginSpecifier,
Expand Down Expand Up @@ -79,6 +80,79 @@ describe("doctor OpenCode legacy agent enabled migration", () => {
});
});

describe("checkUserMemoriesDreamerCompatibility", () => {
const WARNING =
'dreamer.tasks["review-user-memories"] is scheduled but dreamer.disable=true, so new promotions will not run. Remove dreamer.disable or set dreamer.tasks["review-user-memories"].schedule="" to disable the task.';

it("warns when review-user-memories is scheduled and dreamer.disable=true", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: {
disable: true,
tasks: { "review-user-memories": { schedule: "0 2 * * *" } },
},
});
expect(result).toBe(WARNING);
});

it("returns null when dreamer is not disabled", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: {
disable: false,
tasks: { "review-user-memories": { schedule: "0 2 * * *" } },
},
});
expect(result).toBeNull();
});

it("returns null when review-user-memories schedule is empty", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: {
disable: true,
tasks: { "review-user-memories": { schedule: "" } },
},
});
expect(result).toBeNull();
});

it("returns null when review-user-memories schedule is whitespace-only", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: {
disable: true,
tasks: { "review-user-memories": { schedule: " " } },
},
});
expect(result).toBeNull();
});

it("returns null when review-user-memories task is absent", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: { disable: true, tasks: { verify: { schedule: "0 2 * * *" } } },
});
expect(result).toBeNull();
});

it("returns null when dreamer block is absent", () => {
expect(checkUserMemoriesDreamerCompatibility({})).toBeNull();
});

it("returns null when tasks block is absent (legacy v1 shape without user_memories)", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: { disable: true },
});
expect(result).toBeNull();
});

it("does not read legacy dreamer.user_memories (v1 key, migrated away in v2)", () => {
const result = checkUserMemoriesDreamerCompatibility({
dreamer: {
disable: true,
user_memories: { enabled: true },
},
});
expect(result).toBeNull();
});
});

const tempDirs: string[] = [];
const dbs: Database[] = [];
let originalXdgCacheHome: string | undefined;
Expand Down
37 changes: 25 additions & 12 deletions packages/cli/src/commands/doctor-opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ export function migrateLegacyAgentEnabledConfigForDoctor(
return { changed, fixes };
}

/**
* Check whether the `review-user-memories` dreamer task is scheduled while the
* dreamer itself is disabled, a no-op combination where candidate promotions
* will never run. In v2, user-memory collection is gated by the task schedule
* (non-empty = enabled), replacing the v1 `dreamer.user_memories` block.
* Returns the warning message when the combination is wrong, or null otherwise.
*/
export function checkUserMemoriesDreamerCompatibility(
mcConfig: Record<string, unknown>,
): string | null {
const dreamerObj = mcConfig?.dreamer as Record<string, unknown> | undefined;
if (dreamerObj?.disable !== true) return null;
const tasksObj = dreamerObj.tasks as Record<string, unknown> | undefined;
const reviewTask = tasksObj?.["review-user-memories"] as Record<string, unknown> | undefined;
const schedule = reviewTask?.schedule;
if (typeof schedule !== "string" || schedule.trim() === "") return null;
return 'dreamer.tasks["review-user-memories"] is scheduled but dreamer.disable=true, so new promotions will not run. Remove dreamer.disable or set dreamer.tasks["review-user-memories"].schedule="" to disable the task.';
}

/**
* Fetch the latest version of an npm package from the registry. Returns null
* on any error so the doctor can report "check unavailable" rather than fail.
Expand Down Expand Up @@ -1042,23 +1061,17 @@ export async function runDoctor(
}

// 7. Check user memories + dreamer compatibility.
// user_memories graduated from experimental to dreamer.user_memories in
// v0.14, and the default is now enabled. Candidate extraction still
// requires dreamer to actually promote candidates into stable memories,
// In v2, user-memory collection is gated by the `review-user-memories` task
// schedule (non-empty = enabled), replacing the v1 `dreamer.user_memories`
// block. The task needs the dreamer to actually run to promote candidates,
// so warn loudly when the combination is wrong.
if (existsSync(paths.magicContextConfig)) {
try {
const mcRaw = readFileSync(paths.magicContextConfig, "utf-8");
const mcConfig = parse(mcRaw) as Record<string, unknown>;
const dreamerObj = mcConfig?.dreamer as Record<string, unknown> | undefined;
const dreamerDisabled = dreamerObj?.disable === true;
const userMemObj = dreamerObj?.user_memories as Record<string, unknown> | undefined;
// user_memories defaults to enabled, so treat `undefined` as true.
const userMemEnabled = userMemObj?.enabled !== false;
if (userMemEnabled && dreamerDisabled) {
log.warn(
"dreamer.user_memories is enabled but dreamer.disable=true, so new promotions will not run. Remove dreamer.disable or set dreamer.user_memories.enabled=false.",
);
const warning = checkUserMemoriesDreamerCompatibility(mcConfig);
if (warning) {
log.warn(warning);
issues++;
}
} catch {
Expand Down
Loading