From d30ea724692124e46f2f7eeb9cccdeb6691e5b71 Mon Sep 17 00:00:00 2001 From: Duc Tran <90901839+sinhduc01@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:31:23 +0700 Subject: [PATCH] fix(session-history): discover OMP profile session dirs (#40264) * fix(session-history): discover OMP profile session dirs OMP stores per-profile transcripts under /profiles//agent/sessions (XDG_DATA_HOME/omp/profiles//sessions on POSIX with XDG set). The app process usually lacks OMP_PROFILE/PI_PROFILE, so profile-run sessions were missing from Agent Session History, orca search, and native chat. Enumerate existing profile session dirs in ompProfileSessionsDirs() (same env + safety rules as resolveOmpSessionsDir; PI_CONFIG_DIR-aware; suppressed under the legacy OMP_CODING_AGENT_DIR override) and include them in ompSessionsRootDirs() (discovery + path allowlist) and the omp branch of the native chat resolver. An explicit ompSessionsDir override still pins one location. main-sync-fs baseline for omp-session-root.ts 1 -> 3: one readdir of the small fixed profiles dir plus one existsSync candidate check per profile (single call site), the same small-dir probe style the file already uses - not a per-file sweep. Flagged for reviewer sign-off per the ratchet's rule. * fix(session-history): enumerate XDG-only OMP profile dirs ompProfileSessionsDirs only listed profile names from the local config home, so a profile that exists solely under $XDG_DATA_HOME/omp/profiles (e.g. OMP run with a different PI_CONFIG_DIR) was never discovered. Enumerate both roots; per-profile candidate checks and output dedup are unchanged. * docs(session-history): document profile dir helpers for coverage * fix(session-history): use non-blocking profile enumeration in the native chat ID lookup * refactor(session-history): share one OMP root list for discovery and chat lookup Drop the duplicated async profile scan: native chat ID lookup now reuses ompSessionsRootDirs, which already runs the small sync profile probe on the main process for row deletion. Reuse normalizedProfile for directory names and trim tests to the behavior that fails on revert. * fix(session-history): prefer a profile's XDG sessions dir in OMP ID lookup --------- Co-authored-by: ducts Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> --- config/main-sync-fs-baseline.txt | 2 +- .../scanners/omp/omp-session-root.test.ts | 76 ++++++++++++++++++- .../ai-vault/scanners/omp/omp-session-root.ts | 45 ++++++++++- src/main/ai-vault/session-scanner-roots.ts | 8 +- src/main/native-chat/session-file-resolver.ts | 5 +- 5 files changed, 125 insertions(+), 11 deletions(-) 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