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
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user