From 35474d98beea022adfba391e86bd255b70ac5a9d Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 10 Jun 2026 02:22:32 +0200 Subject: [PATCH] refactor(config): moved lastChangelogVersion to marker file - Stored last-seen version in ~/.omp/agent/last-changelog-version so version bumps no longer dirty user configs. - Migrated the legacy config.yml key into the marker, never clobbering a newer existing marker. - Added read/write helpers and migration tests. --- .../src/config/settings-schema.ts | 1 - packages/coding-agent/src/config/settings.ts | 36 +++++++++++++++++++ packages/coding-agent/src/main.ts | 24 ++++++------- packages/coding-agent/src/utils/changelog.ts | 28 ++++++++++++++- .../test/settings-manager.test.ts | 25 +++++++++++++ packages/utils/src/dirs.ts | 5 +++ 6 files changed, 103 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 886f4c518..1067e89d8 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -260,7 +260,6 @@ export const SETTINGS_SCHEMA = { // ──────────────────────────────────────────────────────────────────────── // General settings (no UI) // ──────────────────────────────────────────────────────────────────────── - lastChangelogVersion: { type: "string", default: undefined }, setupVersion: { type: "number", default: 0 }, // Auth broker — credentials proxied through a remote `omp auth-broker serve` diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index 5987395e9..a5ac01950 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -17,6 +17,7 @@ import * as path from "node:path"; import { getAgentDbPath, getAgentDir, + getLastChangelogVersionPath, getProjectDir, isEnoent, logger, @@ -206,6 +207,9 @@ export class Settings { /** Paths modified during this session (for partial save) */ #modified = new Set(); + /** Legacy `lastChangelogVersion` captured from config.yml during migration (now a marker file). */ + #legacyLastChangelogVersion?: string; + /** Pending save (debounced) */ #saveTimer?: NodeJS.Timeout; #savePromise?: Promise; @@ -549,6 +553,7 @@ export class Settings { this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir)); await this.#migrateFromLegacy(); this.#global = await this.#loadYaml(this.#configPath!); + await this.#seedLastChangelogVersionMarker(); } this.#project = await projectPromise; @@ -642,6 +647,16 @@ export class Settings { delete raw.queueMode; } + // lastChangelogVersion moved out of config.yml into the + // /last-changelog-version marker file so version bumps no + // longer dirty user-tracked configs. Capture for marker seeding (see + // #seedLastChangelogVersionMarker), then strip the key — the next + // config save drops it from disk. + if (typeof raw.lastChangelogVersion === "string") { + this.#legacyLastChangelogVersion ??= raw.lastChangelogVersion; + } + delete raw.lastChangelogVersion; + // ask.timeout: ms -> seconds (if value > 1000, it's old ms format) if (raw.ask && typeof (raw.ask as Record).timeout === "number") { const oldValue = (raw.ask as Record).timeout as number; @@ -803,6 +818,27 @@ export class Settings { return raw; } + /** + * One-time migration: seed the last-changelog-version marker file from the + * legacy config.yml key. An existing marker always wins — it is the newer + * source of truth. + */ + async #seedLastChangelogVersionMarker(): Promise { + const legacy = this.#legacyLastChangelogVersion; + if (!legacy) return; + const markerPath = getLastChangelogVersionPath(this.#agentDir); + try { + if ((await Bun.file(markerPath).text()).trim()) return; + } catch (error) { + if (!isEnoent(error)) return; + } + try { + await Bun.write(markerPath, legacy); + } catch (error) { + logger.warn("Settings: failed to seed last-changelog-version marker", { error: String(error) }); + } + } + // ───────────────────────────────────────────────────────────────────────── // Saving // ───────────────────────────────────────────────────────────────────────── diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 91dd2dc07..db7d29315 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -69,7 +69,13 @@ import { resolvePromptInput } from "./system-prompt"; import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export"; import { AUTO_THINKING } from "./thinking"; import type { LspStartupServerInfo } from "./tools"; -import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog"; +import { + getChangelogPath, + getNewEntries, + parseChangelog, + readLastChangelogVersion, + writeLastChangelogVersion, +} from "./utils/changelog"; import { EventBus } from "./utils/event-bus"; type RunAcpMode = (createSession: AcpSessionFactory) => Promise; @@ -506,7 +512,7 @@ async function getChangelogForDisplay(parsed: Args): Promise return undefined; } - const lastVersion = settings.get("lastChangelogVersion"); + const lastVersion = await readLastChangelogVersion(); if (lastVersion === VERSION) { // Steady state: user already saw the current version's changelog. Skip the file read + parse. return undefined; @@ -517,15 +523,13 @@ async function getChangelogForDisplay(parsed: Args): Promise if (!lastVersion) { if (entries.length > 0) { - settings.set("lastChangelogVersion", VERSION); - await flushChangelogVersion(); + await writeLastChangelogVersion(VERSION); return entries.map(e => e.content).join("\n\n"); } } else { const newEntries = getNewEntries(entries, lastVersion); if (newEntries.length > 0) { - settings.set("lastChangelogVersion", VERSION); - await flushChangelogVersion(); + await writeLastChangelogVersion(VERSION); return newEntries.map(e => e.content).join("\n\n"); } } @@ -533,14 +537,6 @@ async function getChangelogForDisplay(parsed: Args): Promise return undefined; } -async function flushChangelogVersion(): Promise { - try { - await settings.flush(); - } catch (error: unknown) { - logger.warn("Failed to persist lastChangelogVersion", { error }); - } -} - /** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */ export async function createSessionManager( parsed: Args, diff --git a/packages/coding-agent/src/utils/changelog.ts b/packages/coding-agent/src/utils/changelog.ts index e931a792a..ac12bb401 100644 --- a/packages/coding-agent/src/utils/changelog.ts +++ b/packages/coding-agent/src/utils/changelog.ts @@ -1,4 +1,4 @@ -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; +import { getLastChangelogVersionPath, isEnoent, logger } from "@oh-my-pi/pi-utils"; export interface ChangelogEntry { major: number; @@ -104,3 +104,29 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C // Re-export getChangelogPath from paths.ts for convenience export { getChangelogPath } from "../config"; + +/** + * Last omp version whose changelog the user has seen. Stored as a plain-text + * marker file (`~/.omp/agent/last-changelog-version`) rather than in + * `config.yml`, so version bumps never dirty user-tracked config files. + */ +export async function readLastChangelogVersion(agentDir?: string): Promise { + try { + const value = (await Bun.file(getLastChangelogVersionPath(agentDir)).text()).trim(); + return value || undefined; + } catch (error) { + if (!isEnoent(error)) { + logger.warn("Failed to read last-changelog-version marker", { error: String(error) }); + } + return undefined; + } +} + +/** Persist the last-seen changelog version marker. Best-effort: failures are logged, never thrown. */ +export async function writeLastChangelogVersion(version: string, agentDir?: string): Promise { + try { + await Bun.write(getLastChangelogVersionPath(agentDir), version); + } catch (error) { + logger.warn("Failed to persist last-changelog-version marker", { error: String(error) }); + } +} diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index e7d972b0e..9aa6e36b6 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -404,5 +404,30 @@ describe("Settings", () => { expect(settings.get("mnemopi.dbPath")).toBe("/tmp/new.db"); }); + + it("moves legacy lastChangelogVersion out of config.yml into the marker file", async () => { + await writeSettings({ lastChangelogVersion: "0.40.0" }); + + const settings = await Settings.init({ cwd: projectDir, agentDir }); + + // Marker seeded from the legacy key. + expect(fs.readFileSync(path.join(agentDir, "last-changelog-version"), "utf8")).toBe("0.40.0"); + + // Key stripped from config.yml on the next save. + settings.set("display.showTokenUsage", true); + await settings.flush(); + const onDisk = await readSettings(); + expect("lastChangelogVersion" in onDisk).toBe(false); + expect((onDisk.display as Record).showTokenUsage).toBe(true); + }); + + it("never clobbers an existing marker with the legacy config value", async () => { + fs.writeFileSync(path.join(agentDir, "last-changelog-version"), "0.41.0"); + await writeSettings({ lastChangelogVersion: "0.40.0" }); + + await Settings.init({ cwd: projectDir, agentDir }); + + expect(fs.readFileSync(path.join(agentDir, "last-changelog-version"), "utf8")).toBe("0.41.0"); + }); }); }); diff --git a/packages/utils/src/dirs.ts b/packages/utils/src/dirs.ts index 507bda3a5..89ff8b3a2 100644 --- a/packages/utils/src/dirs.ts +++ b/packages/utils/src/dirs.ts @@ -398,6 +398,11 @@ export function getAgentDbPath(agentDir?: string): string { return dirs.agentSubdir(agentDir, "agent.db", "data"); } +/** Get the last-seen-changelog-version marker file (~/.omp/agent/last-changelog-version). */ +export function getLastChangelogVersionPath(agentDir?: string): string { + return dirs.agentSubdir(agentDir, "last-changelog-version", "state"); +} + /** Get the path to history.db (SQLite database for session history). */ export function getHistoryDbPath(agentDir?: string): string { return dirs.agentSubdir(agentDir, "history.db", "data");