mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:48:38 +00:00
sessions: canonicalize maintenance path matching
This commit is contained in:
@@ -32,11 +32,21 @@ const NOOP_LOGGER: SessionDiskBudgetLogger = {
|
|||||||
|
|
||||||
type SessionsDirFileStat = {
|
type SessionsDirFileStat = {
|
||||||
path: string;
|
path: string;
|
||||||
|
canonicalPath: string;
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
mtimeMs: number;
|
mtimeMs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function canonicalizePathForComparison(filePath: string): string {
|
||||||
|
const resolved = path.resolve(filePath);
|
||||||
|
try {
|
||||||
|
return fs.realpathSync(resolved);
|
||||||
|
} catch {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function measureStoreBytes(store: Record<string, SessionEntry>): number {
|
function measureStoreBytes(store: Record<string, SessionEntry>): number {
|
||||||
return Buffer.byteLength(JSON.stringify(store, null, 2), "utf-8");
|
return Buffer.byteLength(JSON.stringify(store, null, 2), "utf-8");
|
||||||
}
|
}
|
||||||
@@ -89,12 +99,13 @@ function resolveSessionTranscriptPathForEntry(params: {
|
|||||||
const resolved = resolveSessionFilePath(params.entry.sessionId, params.entry, {
|
const resolved = resolveSessionFilePath(params.entry.sessionId, params.entry, {
|
||||||
sessionsDir: params.sessionsDir,
|
sessionsDir: params.sessionsDir,
|
||||||
});
|
});
|
||||||
const resolvedSessionsDir = path.resolve(params.sessionsDir);
|
const resolvedSessionsDir = canonicalizePathForComparison(params.sessionsDir);
|
||||||
const relative = path.relative(resolvedSessionsDir, path.resolve(resolved));
|
const resolvedPath = canonicalizePathForComparison(resolved);
|
||||||
|
const relative = path.relative(resolvedSessionsDir, resolvedPath);
|
||||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolvedPath;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -111,7 +122,7 @@ function resolveReferencedSessionTranscriptPaths(params: {
|
|||||||
entry,
|
entry,
|
||||||
});
|
});
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
referenced.add(resolved);
|
referenced.add(canonicalizePathForComparison(resolved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return referenced;
|
return referenced;
|
||||||
@@ -133,6 +144,7 @@ async function readSessionsDirFiles(sessionsDir: string): Promise<SessionsDirFil
|
|||||||
}
|
}
|
||||||
files.push({
|
files.push({
|
||||||
path: filePath,
|
path: filePath,
|
||||||
|
canonicalPath: canonicalizePathForComparison(filePath),
|
||||||
name: dirent.name,
|
name: dirent.name,
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
mtimeMs: stat.mtimeMs,
|
mtimeMs: stat.mtimeMs,
|
||||||
@@ -152,20 +164,22 @@ async function removeFileIfExists(filePath: string): Promise<number> {
|
|||||||
|
|
||||||
async function removeFileForBudget(params: {
|
async function removeFileForBudget(params: {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
canonicalPath?: string;
|
||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
fileSizesByPath: Map<string, number>;
|
fileSizesByPath: Map<string, number>;
|
||||||
simulatedRemovedPaths: Set<string>;
|
simulatedRemovedPaths: Set<string>;
|
||||||
}): Promise<number> {
|
}): Promise<number> {
|
||||||
const resolvedPath = path.resolve(params.filePath);
|
const resolvedPath = path.resolve(params.filePath);
|
||||||
|
const canonicalPath = params.canonicalPath ?? canonicalizePathForComparison(resolvedPath);
|
||||||
if (params.dryRun) {
|
if (params.dryRun) {
|
||||||
if (params.simulatedRemovedPaths.has(resolvedPath)) {
|
if (params.simulatedRemovedPaths.has(canonicalPath)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const size = params.fileSizesByPath.get(resolvedPath) ?? 0;
|
const size = params.fileSizesByPath.get(canonicalPath) ?? 0;
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
params.simulatedRemovedPaths.add(resolvedPath);
|
params.simulatedRemovedPaths.add(canonicalPath);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
return removeFileIfExists(resolvedPath);
|
return removeFileIfExists(resolvedPath);
|
||||||
@@ -189,10 +203,10 @@ export async function enforceSessionDiskBudget(params: {
|
|||||||
const dryRun = params.dryRun === true;
|
const dryRun = params.dryRun === true;
|
||||||
const sessionsDir = path.dirname(params.storePath);
|
const sessionsDir = path.dirname(params.storePath);
|
||||||
const files = await readSessionsDirFiles(sessionsDir);
|
const files = await readSessionsDirFiles(sessionsDir);
|
||||||
const fileSizesByPath = new Map(files.map((file) => [path.resolve(file.path), file.size]));
|
const fileSizesByPath = new Map(files.map((file) => [file.canonicalPath, file.size]));
|
||||||
const simulatedRemovedPaths = new Set<string>();
|
const simulatedRemovedPaths = new Set<string>();
|
||||||
const resolvedStorePath = path.resolve(params.storePath);
|
const resolvedStorePath = canonicalizePathForComparison(params.storePath);
|
||||||
const storeFile = files.find((file) => path.resolve(file.path) === resolvedStorePath);
|
const storeFile = files.find((file) => file.canonicalPath === resolvedStorePath);
|
||||||
let projectedStoreBytes = measureStoreBytes(params.store);
|
let projectedStoreBytes = measureStoreBytes(params.store);
|
||||||
let total =
|
let total =
|
||||||
files.reduce((sum, file) => sum + file.size, 0) - (storeFile?.size ?? 0) + projectedStoreBytes;
|
files.reduce((sum, file) => sum + file.size, 0) - (storeFile?.size ?? 0) + projectedStoreBytes;
|
||||||
@@ -241,7 +255,7 @@ export async function enforceSessionDiskBudget(params: {
|
|||||||
.filter(
|
.filter(
|
||||||
(file) =>
|
(file) =>
|
||||||
isSessionArchiveArtifactName(file.name) ||
|
isSessionArchiveArtifactName(file.name) ||
|
||||||
(isPrimarySessionTranscriptFileName(file.name) && !referencedPaths.has(file.path)),
|
(isPrimarySessionTranscriptFileName(file.name) && !referencedPaths.has(file.canonicalPath)),
|
||||||
)
|
)
|
||||||
.toSorted((a, b) => a.mtimeMs - b.mtimeMs);
|
.toSorted((a, b) => a.mtimeMs - b.mtimeMs);
|
||||||
for (const file of removableFileQueue) {
|
for (const file of removableFileQueue) {
|
||||||
@@ -250,6 +264,7 @@ export async function enforceSessionDiskBudget(params: {
|
|||||||
}
|
}
|
||||||
const deletedBytes = await removeFileForBudget({
|
const deletedBytes = await removeFileForBudget({
|
||||||
filePath: file.path,
|
filePath: file.path,
|
||||||
|
canonicalPath: file.canonicalPath,
|
||||||
dryRun,
|
dryRun,
|
||||||
fileSizesByPath,
|
fileSizesByPath,
|
||||||
simulatedRemovedPaths,
|
simulatedRemovedPaths,
|
||||||
|
|||||||
@@ -164,6 +164,15 @@ export function resolveSessionTranscriptCandidates(
|
|||||||
|
|
||||||
export type ArchiveFileReason = SessionArchiveReason;
|
export type ArchiveFileReason = SessionArchiveReason;
|
||||||
|
|
||||||
|
function canonicalizePathForComparison(filePath: string): string {
|
||||||
|
const resolved = path.resolve(filePath);
|
||||||
|
try {
|
||||||
|
return fs.realpathSync(resolved);
|
||||||
|
} catch {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function archiveFileOnDisk(filePath: string, reason: ArchiveFileReason): string {
|
export function archiveFileOnDisk(filePath: string, reason: ArchiveFileReason): string {
|
||||||
const ts = formatSessionArchiveTimestamp();
|
const ts = formatSessionArchiveTimestamp();
|
||||||
const archived = `${filePath}.${reason}.${ts}`;
|
const archived = `${filePath}.${reason}.${ts}`;
|
||||||
@@ -189,24 +198,27 @@ export function archiveSessionTranscripts(opts: {
|
|||||||
}): string[] {
|
}): string[] {
|
||||||
const archived: string[] = [];
|
const archived: string[] = [];
|
||||||
const storeDir =
|
const storeDir =
|
||||||
opts.restrictToStoreDir && opts.storePath ? path.resolve(path.dirname(opts.storePath)) : null;
|
opts.restrictToStoreDir && opts.storePath
|
||||||
|
? canonicalizePathForComparison(path.dirname(opts.storePath))
|
||||||
|
: null;
|
||||||
for (const candidate of resolveSessionTranscriptCandidates(
|
for (const candidate of resolveSessionTranscriptCandidates(
|
||||||
opts.sessionId,
|
opts.sessionId,
|
||||||
opts.storePath,
|
opts.storePath,
|
||||||
opts.sessionFile,
|
opts.sessionFile,
|
||||||
opts.agentId,
|
opts.agentId,
|
||||||
)) {
|
)) {
|
||||||
|
const candidatePath = canonicalizePathForComparison(candidate);
|
||||||
if (storeDir) {
|
if (storeDir) {
|
||||||
const relative = path.relative(storeDir, path.resolve(candidate));
|
const relative = path.relative(storeDir, candidatePath);
|
||||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(candidate)) {
|
if (!fs.existsSync(candidatePath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
archived.push(archiveFileOnDisk(candidate, opts.reason));
|
archived.push(archiveFileOnDisk(candidatePath, opts.reason));
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort.
|
// Best-effort.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user