From cb6310e159bf6cac6fe70297f4a4f8e1d3a6d03c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 26 Dec 2025 03:24:35 +0100 Subject: [PATCH] Add automatic session migration for v0.30.0 bug - Create migrations.ts with consolidated migrations - Move auth migration from AuthStorage.migrateLegacy() to migrations.ts - Add migrateSessionsFromAgentRoot() to fix misplaced sessions - Sessions in ~/.pi/agent/*.jsonl are auto-migrated on startup fixes #320 --- packages/coding-agent/CHANGELOG.md | 6 +- .../coding-agent/src/core/auth-storage.ts | 56 +------- packages/coding-agent/src/main.ts | 8 +- packages/coding-agent/src/migrations.ts | 135 ++++++++++++++++++ 4 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 packages/coding-agent/src/migrations.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4edf4a34..b2e98a9c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,11 +2,15 @@ ## [Unreleased] +### Changed + +- **Consolidated migrations**: Moved auth migration from `AuthStorage.migrateLegacy()` to new `migrations.ts` module. + ## [0.30.1] - 2025-12-26 ### Fixed -- **Sessions saved to wrong directory**: Sessions were being saved to `~/.pi/agent/` instead of `~/.pi/agent/sessions//`, breaking `--resume` and `/resume`. ([#320](https://github.com/badlogic/pi-mono/issues/320) by [@aliou](https://github.com/aliou)) +- **Sessions saved to wrong directory**: In v0.30.0, sessions were being saved to `~/.pi/agent/` instead of `~/.pi/agent/sessions//`, breaking `--resume` and `/resume`. Misplaced sessions are automatically migrated on startup. ([#320](https://github.com/badlogic/pi-mono/issues/320) by [@aliou](https://github.com/aliou)) - **Custom system prompts missing context**: When using a custom system prompt string, project context files (AGENTS.md), skills, date/time, and working directory were not appended. ([#321](https://github.com/badlogic/pi-mono/issues/321)) ## [0.30.0] - 2025-12-25 diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 968b71a1..afc5e076 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -13,8 +13,8 @@ import { type OAuthCredentials, type OAuthProvider, } from "@mariozechner/pi-ai"; -import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs"; -import { dirname, join } from "path"; +import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { dirname } from "path"; export type ApiKeyCredential = { type: "api_key"; @@ -232,56 +232,4 @@ export class AuthStorage { // Fall back to custom resolver (e.g., models.json custom providers) return this.fallbackResolver?.(provider) ?? null; } - - /** - * Migrate credentials from legacy oauth.json and settings.json apiKeys to auth.json. - * Only runs if auth.json doesn't exist yet. Returns list of migrated providers. - */ - static migrateLegacy(authPath: string, agentDir: string): string[] { - const oauthPath = join(agentDir, "oauth.json"); - const settingsPath = join(agentDir, "settings.json"); - - // Skip if auth.json already exists - if (existsSync(authPath)) return []; - - const migrated: AuthStorageData = {}; - const providers: string[] = []; - - // Migrate oauth.json - if (existsSync(oauthPath)) { - try { - const oauth = JSON.parse(readFileSync(oauthPath, "utf-8")); - for (const [provider, cred] of Object.entries(oauth)) { - migrated[provider] = { type: "oauth", ...(cred as object) } as OAuthCredential; - providers.push(provider); - } - renameSync(oauthPath, `${oauthPath}.migrated`); - } catch {} - } - - // Migrate settings.json apiKeys - if (existsSync(settingsPath)) { - try { - const content = readFileSync(settingsPath, "utf-8"); - const settings = JSON.parse(content); - if (settings.apiKeys && typeof settings.apiKeys === "object") { - for (const [provider, key] of Object.entries(settings.apiKeys)) { - if (!migrated[provider] && typeof key === "string") { - migrated[provider] = { type: "api_key", key }; - providers.push(provider); - } - } - delete settings.apiKeys; - writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - } - } catch {} - } - - if (Object.keys(migrated).length > 0) { - mkdirSync(dirname(authPath), { recursive: true }); - writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 }); - } - - return providers; - } } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index fbae4fbe..d9b937b6 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -16,7 +16,7 @@ import { listModels } from "./cli/list-models.js"; import { selectSession } from "./cli/session-picker.js"; import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js"; import type { AgentSession } from "./core/agent-session.js"; -import { AuthStorage } from "./core/auth-storage.js"; + import type { LoadedCustomTool } from "./core/custom-tools/index.js"; import { exportFromFile } from "./core/export-html.js"; import type { HookUIContext } from "./core/index.js"; @@ -28,6 +28,7 @@ import { SettingsManager } from "./core/settings-manager.js"; import { resolvePromptInput } from "./core/system-prompt.js"; import { printTimings, time } from "./core/timings.js"; import { allTools } from "./core/tools/index.js"; +import { runMigrations } from "./migrations.js"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js"; import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js"; @@ -283,9 +284,8 @@ function buildSessionOptions( export async function main(args: string[]) { time("start"); - // Migrate legacy oauth.json and settings.json apiKeys to auth.json - const agentDir = getAgentDir(); - const migratedProviders = AuthStorage.migrateLegacy(join(agentDir, "auth.json"), agentDir); + // Run migrations + const { migratedAuthProviders: migratedProviders } = runMigrations(); // Create AuthStorage and ModelRegistry upfront const authStorage = discoverAuthStorage(); diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts new file mode 100644 index 00000000..f2bda674 --- /dev/null +++ b/packages/coding-agent/src/migrations.ts @@ -0,0 +1,135 @@ +/** + * One-time migrations that run on startup. + */ + +import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { getAgentDir } from "./config.js"; + +/** + * Migrate legacy oauth.json and settings.json apiKeys to auth.json. + * + * @returns Array of provider names that were migrated + */ +export function migrateAuthToAuthJson(): string[] { + const agentDir = getAgentDir(); + const authPath = join(agentDir, "auth.json"); + const oauthPath = join(agentDir, "oauth.json"); + const settingsPath = join(agentDir, "settings.json"); + + // Skip if auth.json already exists + if (existsSync(authPath)) return []; + + const migrated: Record = {}; + const providers: string[] = []; + + // Migrate oauth.json + if (existsSync(oauthPath)) { + try { + const oauth = JSON.parse(readFileSync(oauthPath, "utf-8")); + for (const [provider, cred] of Object.entries(oauth)) { + migrated[provider] = { type: "oauth", ...(cred as object) }; + providers.push(provider); + } + renameSync(oauthPath, `${oauthPath}.migrated`); + } catch { + // Skip on error + } + } + + // Migrate settings.json apiKeys + if (existsSync(settingsPath)) { + try { + const content = readFileSync(settingsPath, "utf-8"); + const settings = JSON.parse(content); + if (settings.apiKeys && typeof settings.apiKeys === "object") { + for (const [provider, key] of Object.entries(settings.apiKeys)) { + if (!migrated[provider] && typeof key === "string") { + migrated[provider] = { type: "api_key", key }; + providers.push(provider); + } + } + delete settings.apiKeys; + writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + } + } catch { + // Skip on error + } + } + + if (Object.keys(migrated).length > 0) { + mkdirSync(dirname(authPath), { recursive: true }); + writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 }); + } + + return providers; +} + +/** + * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories. + * + * Bug in v0.30.0: Sessions were saved to ~/.pi/agent/ instead of + * ~/.pi/agent/sessions//. This migration moves them + * to the correct location based on the cwd in their session header. + * + * See: https://github.com/badlogic/pi-mono/issues/320 + */ +export function migrateSessionsFromAgentRoot(): void { + const agentDir = getAgentDir(); + + // Find all .jsonl files directly in agentDir (not in subdirectories) + let files: string[]; + try { + files = readdirSync(agentDir) + .filter((f) => f.endsWith(".jsonl")) + .map((f) => join(agentDir, f)); + } catch { + return; + } + + if (files.length === 0) return; + + for (const file of files) { + try { + // Read first line to get session header + const content = readFileSync(file, "utf8"); + const firstLine = content.split("\n")[0]; + if (!firstLine?.trim()) continue; + + const header = JSON.parse(firstLine); + if (header.type !== "session" || !header.cwd) continue; + + const cwd: string = header.cwd; + + // Compute the correct session directory (same encoding as session-manager.ts) + const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; + const correctDir = join(agentDir, "sessions", safePath); + + // Create directory if needed + if (!existsSync(correctDir)) { + mkdirSync(correctDir, { recursive: true }); + } + + // Move the file + const fileName = file.split("/").pop() || file.split("\\").pop(); + const newPath = join(correctDir, fileName!); + + if (existsSync(newPath)) continue; // Skip if target exists + + renameSync(file, newPath); + } catch { + // Skip files that can't be migrated + } + } +} + +/** + * Run all migrations. Called once on startup. + * + * @returns Object with migration results + */ +export function runMigrations(): { migratedAuthProviders: string[] } { + const migratedAuthProviders = migrateAuthToAuthJson(); + migrateSessionsFromAgentRoot(); + return { migratedAuthProviders }; +}