mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 13:00:33 +00:00
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
This commit is contained in:
parent
fa946c68fc
commit
cb6310e159
4 changed files with 146 additions and 59 deletions
|
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Consolidated migrations**: Moved auth migration from `AuthStorage.migrateLegacy()` to new `migrations.ts` module.
|
||||||
|
|
||||||
## [0.30.1] - 2025-12-26
|
## [0.30.1] - 2025-12-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Sessions saved to wrong directory**: Sessions were being saved to `~/.pi/agent/` instead of `~/.pi/agent/sessions/<encoded-cwd>/`, 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/<encoded-cwd>/`, 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))
|
- **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
|
## [0.30.0] - 2025-12-25
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
type OAuthCredentials,
|
type OAuthCredentials,
|
||||||
type OAuthProvider,
|
type OAuthProvider,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { dirname, join } from "path";
|
import { dirname } from "path";
|
||||||
|
|
||||||
export type ApiKeyCredential = {
|
export type ApiKeyCredential = {
|
||||||
type: "api_key";
|
type: "api_key";
|
||||||
|
|
@ -232,56 +232,4 @@ export class AuthStorage {
|
||||||
// Fall back to custom resolver (e.g., models.json custom providers)
|
// Fall back to custom resolver (e.g., models.json custom providers)
|
||||||
return this.fallbackResolver?.(provider) ?? null;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { listModels } from "./cli/list-models.js";
|
||||||
import { selectSession } from "./cli/session-picker.js";
|
import { selectSession } from "./cli/session-picker.js";
|
||||||
import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
||||||
import type { AgentSession } from "./core/agent-session.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 type { LoadedCustomTool } from "./core/custom-tools/index.js";
|
||||||
import { exportFromFile } from "./core/export-html.js";
|
import { exportFromFile } from "./core/export-html.js";
|
||||||
import type { HookUIContext } from "./core/index.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 { resolvePromptInput } from "./core/system-prompt.js";
|
||||||
import { printTimings, time } from "./core/timings.js";
|
import { printTimings, time } from "./core/timings.js";
|
||||||
import { allTools } from "./core/tools/index.js";
|
import { allTools } from "./core/tools/index.js";
|
||||||
|
import { runMigrations } from "./migrations.js";
|
||||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||||
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
|
||||||
|
|
@ -283,9 +284,8 @@ function buildSessionOptions(
|
||||||
export async function main(args: string[]) {
|
export async function main(args: string[]) {
|
||||||
time("start");
|
time("start");
|
||||||
|
|
||||||
// Migrate legacy oauth.json and settings.json apiKeys to auth.json
|
// Run migrations
|
||||||
const agentDir = getAgentDir();
|
const { migratedAuthProviders: migratedProviders } = runMigrations();
|
||||||
const migratedProviders = AuthStorage.migrateLegacy(join(agentDir, "auth.json"), agentDir);
|
|
||||||
|
|
||||||
// Create AuthStorage and ModelRegistry upfront
|
// Create AuthStorage and ModelRegistry upfront
|
||||||
const authStorage = discoverAuthStorage();
|
const authStorage = discoverAuthStorage();
|
||||||
|
|
|
||||||
135
packages/coding-agent/src/migrations.ts
Normal file
135
packages/coding-agent/src/migrations.ts
Normal file
|
|
@ -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<string, unknown> = {};
|
||||||
|
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/<encoded-cwd>/. 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 };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue