agent-ecosystem/src/main/services/discovery/ProjectPathResolver.ts
matt 2193b2ed8a feat(sessions): add support for fetching sessions by IDs
- Implemented a new HTTP endpoint and IPC handler for retrieving session metadata based on an array of session IDs, allowing for the loading of pinned sessions that may not be included in paginated results.
- Enhanced session management by validating session IDs and capping the number of IDs processed to improve performance and reliability.
- Updated the renderer and store to handle the new functionality, ensuring that pinned sessions are preserved and loaded correctly.

This commit enhances the application's session handling capabilities, providing users with better access to their pinned sessions.
2026-02-12 17:53:19 +09:00

125 lines
3.9 KiB
TypeScript

/**
* ProjectPathResolver - Resolves encoded project IDs to canonical filesystem paths.
*
* Resolution order:
* 1) cwd hint (if provided and absolute)
* 2) cwd extracted from session JSONL files (authoritative)
* 3) decodePath(projectId) fallback (lossy, best-effort)
*
* Results are memoized per projectId and can be invalidated by file watcher events.
*/
import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFileSystemProvider';
import { extractCwd } from '@main/utils/jsonl';
import { decodePath, extractBaseDir, getProjectsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as path from 'path';
import { subprojectRegistry } from './SubprojectRegistry';
import type { FileSystemProvider } from '@main/services/infrastructure/FileSystemProvider';
const logger = createLogger('Discovery:ProjectPathResolver');
interface ResolveProjectPathOptions {
cwdHint?: string;
sessionPaths?: string[];
forceRefresh?: boolean;
}
export class ProjectPathResolver {
private readonly projectsDir: string;
private readonly fsProvider: FileSystemProvider;
private readonly projectPathCache = new Map<string, string>();
constructor(projectsDir?: string, fsProvider?: FileSystemProvider) {
this.projectsDir = projectsDir ?? getProjectsBasePath();
this.fsProvider = fsProvider ?? new LocalFileSystemProvider();
}
/**
* Resolve a project ID to a canonical path.
*/
async resolveProjectPath(
projectId: string,
options?: ResolveProjectPathOptions
): Promise<string> {
const opts = options ?? {};
// Short-circuit for composite IDs: use the registry's cwd directly
const registryCwd = subprojectRegistry.getCwd(projectId);
if (registryCwd) {
this.projectPathCache.set(projectId, registryCwd);
return registryCwd;
}
if (!opts.forceRefresh) {
const cached = this.projectPathCache.get(projectId);
if (cached) {
return cached;
}
}
const cwdHint = opts.cwdHint?.trim();
if (cwdHint && path.isAbsolute(cwdHint)) {
this.projectPathCache.set(projectId, cwdHint);
return cwdHint;
}
const sessionPaths = opts.sessionPaths?.length
? opts.sessionPaths
: await this.listSessionPaths(projectId);
// In SSH mode, avoid scanning every remote session file just to resolve display path.
// One successful cwd extraction is sufficient.
const maxPathsToInspect = this.fsProvider.type === 'ssh' ? 1 : sessionPaths.length;
for (const sessionPath of sessionPaths.slice(0, maxPathsToInspect)) {
try {
const cwd = await extractCwd(sessionPath, this.fsProvider);
if (cwd && path.isAbsolute(cwd)) {
this.projectPathCache.set(projectId, cwd);
return cwd;
}
} catch {
// Ignore unreadable or malformed files and continue to next candidate.
}
}
const decoded = decodePath(extractBaseDir(projectId));
this.projectPathCache.set(projectId, decoded);
return decoded;
}
/**
* Invalidate a single project's cached path.
*/
invalidateProject(projectId: string): void {
this.projectPathCache.delete(projectId);
}
/**
* Clear all cached project paths.
*/
clear(): void {
this.projectPathCache.clear();
}
private async listSessionPaths(projectId: string): Promise<string[]> {
const projectDir = path.join(this.projectsDir, extractBaseDir(projectId));
if (!(await this.fsProvider.exists(projectDir))) {
return [];
}
try {
const entries = await this.fsProvider.readdir(projectDir);
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
.map((entry) => path.join(projectDir, entry.name));
} catch (error) {
logger.error(`Failed to read session files for ${projectId}:`, error);
return [];
}
}
}
export const projectPathResolver = new ProjectPathResolver();