Merge pull request #7656 from can1357/farm/95e5830c/revert-7397

revert(session): restore legacy project directory names
This commit is contained in:
Can Bölük
2026-08-04 23:13:55 +02:00
committed by GitHub
3 changed files with 76 additions and 169 deletions
+14
View File
@@ -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
@@ -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<string>();
/**
* 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 `--<home-encoded>-*--` 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;
}
@@ -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", () => {