fix(session-history): discover OMP profile session dirs (#40264)

* fix(session-history): discover OMP profile session dirs

OMP stores per-profile transcripts under <config home>/profiles/<name>/agent/sessions
(XDG_DATA_HOME/omp/profiles/<name>/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 <ducts@fpt.com>
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
This commit is contained in:
Duc Tran
2026-11-11 05:31:23 +00:00
committed by GitHub
co-authored by ducts Kelvin Amoaba
parent b7e0fa8e22
commit d30ea72469
5 changed files with 125 additions and 11 deletions
+1 -1
View File
@@ -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
@@ -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)
})
})
@@ -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 []
}
}
+5 -3
View File
@@ -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.
@@ -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