diff --git a/config/main-sync-fs-baseline.txt b/config/main-sync-fs-baseline.txt index 3437945403d..16885fe4f6e 100644 --- a/config/main-sync-fs-baseline.txt +++ b/config/main-sync-fs-baseline.txt @@ -18,7 +18,7 @@ 4 src/main/ai-vault/parse-cache/session-parse-cache-legacy-json.ts 2 src/main/ai-vault/scan-service/session-scanner-service-entry-path.ts 1 src/main/ai-vault/scan-service/session-scanner-service-spawn.ts -1 src/main/ai-vault/scanners/omp/omp-session-root.ts +3 src/main/ai-vault/scanners/omp/omp-session-root.ts # small fixed profile-dir probes mirroring OMP dirs.ts; one readdir + per-profile candidate checks, not a per-file sweep 1 src/main/ai-vault/scanners/opencode/reader-transport/opencode-wsl-runtime-preparation.ts 2 src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-sqlite-worker-spawn.ts 8 src/main/amp/hook-service.ts diff --git a/src/main/ai-vault/scanners/omp/omp-session-root.test.ts b/src/main/ai-vault/scanners/omp/omp-session-root.test.ts index 1716627e40c..93b2df12eb1 100644 --- a/src/main/ai-vault/scanners/omp/omp-session-root.test.ts +++ b/src/main/ai-vault/scanners/omp/omp-session-root.test.ts @@ -1,8 +1,8 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import { resolveOmpSessionsDir } from './omp-session-root' +import { ompProfileSessionsDirs, resolveOmpSessionsDir } from './omp-session-root' import { ompSessionsRootDirs } from '../../session-scanner-roots' import { AI_VAULT_AGENT_SOURCES } from '../../session-scanner-agent-sources' import { resolveSessionFilePath } from '../../../native-chat/session-file-resolver' @@ -36,7 +36,7 @@ describe('OMP session root parity', () => { } ) it('uses the same live default for discovery, path allowlisting and native chat', async () => { - const { xdg } = fixture() + const { home, xdg } = fixture() const sessions = join(xdg, 'omp', 'sessions') mkdirSync(join(sessions, 'repo'), { recursive: true }) const transcript = join(sessions, 'repo', 'stamp_omp-session.jsonl') @@ -50,6 +50,8 @@ describe('OMP session root parity', () => { ]) { vi.stubEnv(key, undefined) } + // Pin discovery to the fixture home so real profile dirs cannot leak in. + vi.stubEnv('HOME', home) vi.stubEnv('XDG_DATA_HOME', xdg) if (process.platform === 'win32') { return @@ -65,3 +67,71 @@ describe('OMP session root parity', () => { ).resolves.toBeNull() }) }) + +describe('ompProfileSessionsDirs', () => { + it('lists only existing session dirs of valid profiles', () => { + const { home } = fixture() + const valid = join(home, '.omp', 'profiles', 'qwen', 'agent', 'sessions') + mkdirSync(valid, { recursive: true }) + mkdirSync(join(home, '.omp', 'profiles', 'empty-profile', 'agent'), { recursive: true }) + mkdirSync(join(home, '.omp', 'profiles', 'work.', 'agent', 'sessions'), { recursive: true }) + mkdirSync(join(home, '.omp', 'profiles', 'default', 'agent', 'sessions'), { recursive: true }) + writeFileSync(join(home, '.omp', 'profiles', 'stray-file'), '') + expect(ompProfileSessionsDirs({ env: {}, homeDir: home, platform: 'linux' })).toEqual([valid]) + }) + + it('adds XDG data home profile dirs on POSIX only', () => { + const { home, xdg } = fixture() + const local = join(home, '.omp', 'profiles', 'qwen', 'agent', 'sessions') + const xdgVariant = join(xdg, 'omp', 'profiles', 'qwen', 'sessions') + const xdgOnly = join(xdg, 'omp', 'profiles', 'fast', 'sessions') + for (const dir of [local, xdgVariant, xdgOnly]) { + mkdirSync(dir, { recursive: true }) + } + expect( + ompProfileSessionsDirs({ env: { XDG_DATA_HOME: xdg }, homeDir: home, platform: 'linux' }) + ).toEqual([xdgOnly, xdgVariant, local]) + expect( + ompProfileSessionsDirs({ env: { XDG_DATA_HOME: xdg }, homeDir: home, platform: 'win32' }) + ).toEqual([local]) + }) + + it('stays empty under the legacy OMP_CODING_AGENT_DIR override', () => { + const { home } = fixture() + mkdirSync(join(home, '.omp', 'profiles', 'qwen', 'agent', 'sessions'), { recursive: true }) + expect( + ompProfileSessionsDirs({ + env: { OMP_CODING_AGENT_DIR: '/opt/omp' }, + homeDir: home, + platform: 'linux' + }) + ).toEqual([]) + }) +}) + +describe('OMP profile history parity', () => { + it('discovers profile runs the app process did not launch', async () => { + const { home } = fixture() + const profileSessions = join(home, '.omp', 'profiles', 'qwen', 'agent', 'sessions') + const transcript = join(profileSessions, 'repo', 'stamp_profile-session.jsonl') + mkdirSync(dirname(transcript), { recursive: true }) + writeFileSync(transcript, '{}\n') + for (const key of [ + 'OMP_CODING_AGENT_DIR', + 'PI_CODING_AGENT_DIR', + 'OMP_PROFILE', + 'PI_PROFILE', + 'PI_CONFIG_DIR', + 'XDG_DATA_HOME' + ]) { + vi.stubEnv(key, undefined) + } + vi.stubEnv('HOME', home) + if (process.platform === 'win32') { + return + } + expect(ompSessionsRootDirs({})).toContain(profileSessions) + expect(AI_VAULT_AGENT_SOURCES.omp?.rootDirs({}, [])).toContain(profileSessions) + await expect(resolveSessionFilePath('omp', 'profile-session')).resolves.toBe(transcript) + }) +}) diff --git a/src/main/ai-vault/scanners/omp/omp-session-root.ts b/src/main/ai-vault/scanners/omp/omp-session-root.ts index 13dcfa3faac..46a6ce3773e 100644 --- a/src/main/ai-vault/scanners/omp/omp-session-root.ts +++ b/src/main/ai-vault/scanners/omp/omp-session-root.ts @@ -1,4 +1,4 @@ -import { existsSync } from 'node:fs' +import { existsSync, readdirSync } from 'node:fs' import { homedir } from 'node:os' import { join, normalize, parse, resolve, win32 } from 'node:path' import { normalizeAgentSessionsDir } from '../../session-scanner-values' @@ -78,3 +78,46 @@ export function resolveOmpSessionsDir( } return safeRoot(join(agentDir, 'sessions')) } + +/** Existing session dirs of named profiles: each OMP run picks its profile from its own env, not Orca's. */ +export function ompProfileSessionsDirs( + options: { + env?: NodeJS.ProcessEnv + homeDir?: string + platform?: NodeJS.Platform + } = {} +): string[] { + const env = options.env ?? process.env + const home = options.homeDir ?? homedir() + const platform = options.platform ?? process.platform + // The legacy override pins one location; enumeration would widen it. + if (env.OMP_CODING_AGENT_DIR?.trim()) { + return [] + } + const profilesDir = join(home, env.PI_CONFIG_DIR || '.omp', 'profiles') + const xdgProfilesDir = + (platform === 'linux' || platform === 'darwin') && env.XDG_DATA_HOME + ? join(env.XDG_DATA_HOME, 'omp', 'profiles') + : undefined + const names = new Set( + [profilesDir, xdgProfilesDir].flatMap((dir) => (dir ? profileDirNames(dir) : [])) + ) + return [...names] + .sort() + .flatMap((name) => [ + // OMP writes here once the profile's XDG dir exists, so ID lookup must find it before older copies. + ...(xdgProfilesDir ? [join(xdgProfilesDir, name, 'sessions')] : []), + join(profilesDir, name, 'agent', 'sessions') + ]) + .filter((dir) => existsSync(dir)) +} + +function profileDirNames(profilesDir: string): string[] { + try { + return readdirSync(profilesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && normalizedProfile(entry.name) === entry.name) + .map((entry) => entry.name) + } catch { + return [] + } +} diff --git a/src/main/ai-vault/session-scanner-roots.ts b/src/main/ai-vault/session-scanner-roots.ts index d69230d1609..d82c83a0759 100644 --- a/src/main/ai-vault/session-scanner-roots.ts +++ b/src/main/ai-vault/session-scanner-roots.ts @@ -3,7 +3,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import { resolveAbsoluteDirOverride } from '../../shared/absolute-dir-override' import { WSL_CODEX_RUNTIME_HOME_SEGMENTS } from '../pty/shell-startup/codex-home-wsl-env' -import { resolveOmpSessionsDir } from './scanners/omp/omp-session-root' +import { ompProfileSessionsDirs, resolveOmpSessionsDir } from './scanners/omp/omp-session-root' /** Where each transcript agent's session tree sits under a home dir (host or WSL guest). */ export const AGENT_SESSION_HOME_SEGMENTS = { @@ -63,14 +63,16 @@ export function uniqueDirs(dirs: readonly string[]): string[] { return dirs.filter((dir, index) => dirs.indexOf(dir) === index) } -// Use these same roots for discovery and transcript path validation. +// Use these same roots for discovery, transcript path validation and native chat ID lookup. export function ompSessionsRootDirs(args: { ompSessionsDir?: string wslHomeDirs?: readonly string[] }): string[] { - return ( + return uniqueDirs( [ resolveOmpSessionsDir({ sessionsDir: args.ompSessionsDir }), + // An explicit sessions dir pins one location. + ...(args.ompSessionsDir === undefined ? ompProfileSessionsDirs() : []), ...wslHomeSessionDirs('omp', normalizedWslHomeDirs(args.wslHomeDirs)) ] // Reject empty roots: normalizing OMP_CODING_AGENT_DIR=/ can otherwise allowlist the process cwd. diff --git a/src/main/native-chat/session-file-resolver.ts b/src/main/native-chat/session-file-resolver.ts index 2171b20dff2..6af57f9d6b3 100644 --- a/src/main/native-chat/session-file-resolver.ts +++ b/src/main/native-chat/session-file-resolver.ts @@ -8,10 +8,10 @@ import { } from '../../shared/native-chat/native-chat-agent-support' import { isWslUncPath } from '../../shared/wsl-paths' import { forEachSessionFile } from '../ai-vault/session-scanner-discovery' -import { resolveOmpSessionsDir } from '../ai-vault/scanners/omp/omp-session-root' import { claudeProjectsHostDirs, codexHomeSessionsDir, + ompSessionsRootDirs, uniqueDirs } from '../ai-vault/session-scanner-roots' import { resolveOrcaManagedCodexHomePath } from '../codex/configuration/homes/codex-home-paths' @@ -181,8 +181,7 @@ async function resolveSessionFileById( } if (transcriptAgent === 'omp') { const override = options.ompSessionsDir - const root = resolveOmpSessionsDir({ sessionsDir: override }) - return resolveTiered('omp', trimmedId, root ? [root] : [], { + return resolveTiered('omp', trimmedId, ompSessionsRootDirs({ ompSessionsDir: override }), { wslTier: wslTier && override === undefined, signal, strictErrors: options.strictErrors