From 172ce9bf6c6bbc95beaaf4898dc1a7f554519a8b Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 4 Aug 2026 21:09:19 +0000 Subject: [PATCH] revert(session): restored legacy project directory names Reverted PR #7397 session directory hashing and automatic migration. Restored existing script compatibility with the legacy cwd-derived naming scheme. Fixes #7646 --- packages/coding-agent/CHANGELOG.md | 14 ++ .../coding-agent/src/session/session-paths.ts | 179 ++++++------------ .../session-manager/file-operations.test.ts | 52 +---- 3 files changed, 76 insertions(+), 169 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 22a72debb..00b5106e4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Restored the legacy project-scoped session directory naming scheme and removed its automatic migration ([#7646](https://github.com/can1357/oh-my-pi/issues/7646)). + ## [17.2.8] - 2026-08-04 ### Changed @@ -92,6 +96,16 @@ - Fixed heavily branched conversation trees shifting linear continuations into disconnected columns. - Fixed plugin installation validation failures for legacy compatibility shims. - Removed hard-coded references to disabled or absent agents in system and tool prompts. +- Fixed `omp setup python` to validate the same configured or discovered interpreter used by the Python eval runtime. + + +### Fixed + +- Fixed self-update misclassifying glibc Linux hosts with an installed musl loader as musl hosts, which could download an unusable musl binary instead of the glibc release. + +### Fixed + +- Fixed a crash where opening the Agent Hub after a resume and moving the selection triggered an unbounded `ExtensionExitError` unhandled-rejection storm and exit 129. The postmortem module bound the native hard-exit at first evaluation; when the bundler deferred that evaluation into a `withHostGuard` window it froze the guard's throwing replacement, poisoning every later signal/fatal exit. The native exit is now resolved per call, and the guard stamps its replacement with the native primitive it shadows so mid-guard signals still exit ([#7393](https://github.com/can1357/oh-my-pi/issues/7393)). ## [17.2.4] - 2026-08-01 diff --git a/packages/coding-agent/src/session/session-paths.ts b/packages/coding-agent/src/session/session-paths.ts index 9f47d02e9..237aa013e 100644 --- a/packages/coding-agent/src/session/session-paths.ts +++ b/packages/coding-agent/src/session/session-paths.ts @@ -2,17 +2,10 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { getTerminalId } from "@oh-my-pi/pi-tui"; -import { - getSessionsDir, - getTerminalSessionsDir, - isEnoent, - isRecord, - logger, - resolveEquivalentPath, -} from "@oh-my-pi/pi-utils"; +import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils"; import type { SessionStorage } from "./session-storage"; -const SESSION_HEADER_PREFIX_BYTES = 4096; +const migratedSessionRoots = new Set(); /** * Merge or rename a legacy session directory into its canonical target. @@ -42,16 +35,12 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string { return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; } -function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string { +function encodeRelativeSessionDirName(prefix: string, relative: string): string { const encoded = relative.replace(/[/\\:]/g, "-"); return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix; } -function getDefaultSessionDirName(cwd: string): { - encodedDirName: string; - legacyRelativeDirName: string | undefined; - resolvedCwd: string; -} { +function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } { const resolvedCwd = path.resolve(cwd); const canonicalCwd = resolveEquivalentPath(resolvedCwd); const home = os.homedir(); @@ -60,111 +49,72 @@ function getDefaultSessionDirName(cwd: string): { const canonicalTempRoot = resolveEquivalentPath(tempRoot); const homeRelative = path.relative(canonicalHome, canonicalCwd); const tempRelative = path.relative(canonicalTempRoot, canonicalCwd); - - let scope: "home" | "tmp" | "abs"; - let legacyRelativeDirName: string | undefined; - if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) { - scope = "home"; - legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative); - } else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) { - scope = "tmp"; - legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative); - } else { - scope = "abs"; - } - - const normalized = canonicalCwd.replaceAll("\\", "/"); - const readable = path - .basename(canonicalCwd) - .replace(/[^a-zA-Z0-9._-]+/g, "-") - .replace(/^-+|-+$/g, "") - .slice(-80); - const digest = Bun.SHA256.hash(normalized, "hex"); - const encodedDirName = `${scope}-${readable || "project"}-${digest}`; - return { encodedDirName, legacyRelativeDirName, resolvedCwd }; + const encodedDirName = + homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative)) + ? encodeRelativeSessionDirName("-", homeRelative) + : tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative)) + ? encodeRelativeSessionDirName("-tmp", tempRelative) + : encodeLegacyAbsoluteSessionDirName(canonicalCwd); + return { encodedDirName, resolvedCwd }; } -function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined { - let descriptor: number | undefined; +/** + * Migrate old `---*--` session dirs to the new `-*` format. + * Runs once per sessions root on first access, best-effort. + */ +function migrateHomeSessionDirs(sessionsRoot: string): void { + if (migratedSessionRoots.has(sessionsRoot)) return; + migratedSessionRoots.add(sessionsRoot); + + const home = os.homedir(); + const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-"); + const oldPrefix = `--${homeEncoded}-`; + const oldExact = `--${homeEncoded}--`; + + let entries: string[]; try { - descriptor = fs.openSync(sessionFile, "r"); - const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0); - const prefix = buffer.toString("utf8", 0, bytesRead); - for (const line of prefix.split(/\r?\n/, 3)) { - try { - const record: unknown = JSON.parse(line); - if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") { - return record.cwd; - } - } catch { - // Ignore title slots or truncated/corrupt headers. - } - } + entries = fs.readdirSync(sessionsRoot); } catch { - // Best-effort migration leaves unreadable sessions in the current bucket. - } finally { - if (descriptor !== undefined) fs.closeSync(descriptor); + return; } - return undefined; -} -function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void { - fs.mkdirSync(targetDir, { recursive: true }); - const artifactsName = path.basename(sessionName, ".jsonl"); for (const entry of entries) { - if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue; - const source = path.join(sourceDir, entry); - if (!fs.existsSync(source)) continue; - const target = path.join(targetDir, entry); - const existing = fs.statSync(target, { throwIfNoEntry: false }); - if (!existing) { - fs.renameSync(source, target); - } else if (existing.isDirectory() && fs.statSync(source).isDirectory()) { - migrateSessionDirPath(source, target); + let remainder: string; + if (entry === oldExact) { + remainder = ""; + } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) { + remainder = entry.slice(oldPrefix.length, -2); } else { - fs.rmSync(source, { recursive: true, force: true }); - } - } -} - -function rerouteCollidingSessions( - cwd: string, - legacyDir: string, - sessionsRoot: string, - kind: "relative" | "absolute", -): void { - const entries = fs.readdirSync(legacyDir); - const currentCwd = resolveEquivalentPath(path.resolve(cwd)); - const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES); - for (const entry of entries) { - if (!entry.endsWith(".jsonl")) continue; - const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer); - if (!recordedCwd) continue; - const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd)); - if ( - recordedCanonical === currentCwd || - !fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory() - ) { continue; } - const recordedNames = getDefaultSessionDirName(recordedCanonical); - const recordedLegacyName = - kind === "relative" - ? recordedNames.legacyRelativeDirName - : encodeLegacyAbsoluteSessionDirName(recordedCanonical); - if (recordedLegacyName !== path.basename(legacyDir)) continue; - moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries); + + const newName = remainder ? `-${remainder}` : "-"; + const oldPath = path.join(sessionsRoot, entry); + const newPath = path.join(sessionsRoot, newName); + + try { + migrateSessionDirPath(oldPath, newPath); + } catch { + // Best effort + } + } +} + +function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void { + const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd)); + if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return; + + try { + migrateSessionDirPath(legacyDir, sessionDir); + } catch { + // Best effort } } export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined { const currentDirName = path.basename(sessionDir); - const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd); - if ( - currentDirName !== encodedDirName && - currentDirName !== legacyRelativeDirName && - currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd) - ) { + const { encodedDirName } = getDefaultSessionDirName(cwd); + if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) { return undefined; } return path.dirname(sessionDir); @@ -180,23 +130,10 @@ export function computeDefaultSessionDir( storage: SessionStorage, sessionsRoot: string = getSessionsDir(), ): string { - const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd); + const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd); + migrateHomeSessionDirs(sessionsRoot); const sessionDir = path.join(sessionsRoot, encodedDirName); - const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [ - { kind: "relative", name: legacyRelativeDirName }, - { kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) }, - ]; - for (const legacy of legacyDirs) { - if (!legacy.name) continue; - const legacyDir = path.join(sessionsRoot, legacy.name); - if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue; - try { - rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind); - migrateSessionDirPath(legacyDir, sessionDir); - } catch { - // Best effort - } - } + migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot); storage.ensureDirSync(sessionDir); return sessionDir; } diff --git a/packages/coding-agent/test/session-manager/file-operations.test.ts b/packages/coding-agent/test/session-manager/file-operations.test.ts index e5fc9ebc2..7c3abe61f 100644 --- a/packages/coding-agent/test/session-manager/file-operations.test.ts +++ b/packages/coding-agent/test/session-manager/file-operations.test.ts @@ -6,14 +6,7 @@ import type { FileEntry, SessionHeader } from "@oh-my-pi/pi-coding-agent/session import { findMostRecentSession, resolveResumableSession } from "@oh-my-pi/pi-coding-agent/session/session-listing"; import { loadEntriesFromFile } from "@oh-my-pi/pi-coding-agent/session/session-loader"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { - getConfigRootDir, - getSessionsDir, - removeSyncWithRetries, - resolveEquivalentPath, - Snowflake, - setAgentDir, -} from "@oh-my-pi/pi-utils"; +import { getConfigRootDir, getSessionsDir, removeSyncWithRetries, Snowflake, setAgentDir } from "@oh-my-pi/pi-utils"; describe("loadEntriesFromFile", () => { let tempDir: string; @@ -170,9 +163,7 @@ describe("SessionManager temp cwd session dirs", () => { const fallbackAgentDir = path.join(getConfigRootDir(), "agent"); function expectedTempSessionDirName(tempCwd: string): string { - const normalized = resolveEquivalentPath(tempCwd).replaceAll("\\", "/"); - const digest = Bun.SHA256.hash(normalized, "hex"); - return `tmp-${path.basename(tempCwd)}-${digest}`; + return `-tmp-${path.relative(os.tmpdir(), path.resolve(tempCwd)).replace(/[/\\:]/g, "-")}`; } function toLegacyAbsoluteSessionDirName(cwd: string): string { @@ -197,7 +188,7 @@ describe("SessionManager temp cwd session dirs", () => { removeSyncWithRetries(testAgentDir); }); - it("stores temp-root cwd sessions under safe hashed directories", () => { + it("stores temp-root cwd sessions under -tmp-prefixed directories", () => { const tempCwd = path.join(testAgentDir, `temp-cwd-${Snowflake.next()}`); fs.mkdirSync(tempCwd, { recursive: true }); @@ -208,7 +199,7 @@ describe("SessionManager temp cwd session dirs", () => { expect(path.dirname(sessionFile)).toBe(path.join(getSessionsDir(), expectedTempSessionDirName(tempCwd))); }); - it("migrates legacy temp-root absolute session dirs to safe names", () => { + it("migrates legacy temp-root absolute session dirs to -tmp prefixes", () => { const tempCwd = path.join(testAgentDir, `legacy-cwd-${Snowflake.next()}`); fs.mkdirSync(tempCwd, { recursive: true }); @@ -226,41 +217,6 @@ describe("SessionManager temp cwd session dirs", () => { expect(path.dirname(sessionFile)).toBe(expectedDir); expect(fs.existsSync(path.join(expectedDir, "carried.jsonl"))).toBe(true); }); - - it("separates colliding legacy cwd buckets into safe directories", () => { - const firstCwd = path.join(testAgentDir, "project", "hail-mary"); - const secondCwd = path.join(testAgentDir, "project-hail-mary"); - fs.mkdirSync(firstCwd, { recursive: true }); - fs.mkdirSync(secondCwd, { recursive: true }); - - const legacyName = `-tmp-${path.relative(os.tmpdir(), firstCwd).replace(/[/\\:]/g, "-")}`; - expect(legacyName).toBe(`-tmp-${path.relative(os.tmpdir(), secondCwd).replace(/[/\\:]/g, "-")}`); - const legacyDir = path.join(getSessionsDir(), legacyName); - fs.mkdirSync(path.join(legacyDir, "first"), { recursive: true }); - fs.mkdirSync(path.join(legacyDir, "second"), { recursive: true }); - fs.writeFileSync( - path.join(legacyDir, "first.jsonl"), - `${JSON.stringify({ type: "session", id: "first", cwd: firstCwd })}\n`, - ); - fs.writeFileSync( - path.join(legacyDir, "second.jsonl"), - `${JSON.stringify({ type: "session", id: "second", cwd: secondCwd })}\n`, - ); - fs.writeFileSync(path.join(legacyDir, "first", "artifact.txt"), "first"); - fs.writeFileSync(path.join(legacyDir, "second", "artifact.txt"), "second"); - - const firstDir = path.dirname(SessionManager.create(firstCwd).getSessionFile()!); - const secondDir = path.dirname(SessionManager.create(secondCwd).getSessionFile()!); - - expect(firstDir).not.toBe(secondDir); - expect(path.basename(firstDir).startsWith("-")).toBe(false); - expect(path.basename(secondDir).startsWith("-")).toBe(false); - expect(fs.existsSync(path.join(firstDir, "first.jsonl"))).toBe(true); - expect(fs.existsSync(path.join(firstDir, "first", "artifact.txt"))).toBe(true); - expect(fs.existsSync(path.join(secondDir, "second.jsonl"))).toBe(true); - expect(fs.existsSync(path.join(secondDir, "second", "artifact.txt"))).toBe(true); - expect(fs.existsSync(legacyDir)).toBe(false); - }); }); describe("SessionManager legacy session migration persistence", () => {