mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
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:
co-authored by
ducts
Kelvin Amoaba
parent
b7e0fa8e22
commit
d30ea72469
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user