fix(omp): find relocated conversations in relay history (#20638)

* fix(omp): resolve relay history roots on the execution host

* refactor(ai-vault): inline fixed Pi and OMP root segments

* refactor(ai-vault): keep fixed root segments compact
This commit is contained in:
Neil
2026-09-19 00:17:52 -07:00
committed by GitHub
parent acd91035c0
commit 5a67404783
8 changed files with 350 additions and 34 deletions
+36
View File
@@ -0,0 +1,36 @@
# OMP relay history roots
The relay's history sidecar now resolves OMP storage on the execution host using
`resolveOmpSessionsDir`, the same resolver as the native reader and local history
scanner. Its environment allowlist carries only OMP root/profile inputs alongside
the existing runtime variables. Node loader flags and unrelated credentials remain
excluded.
The selected root replaces the legacy OMP source; it does not add a second scan.
An invalid profile/root disables that source rather than selecting another store.
The same bounded discovery, cancellation, child partition and parse cache remain.
There are no new subprocesses, polls, recursive scan roots or wire fields.
The optional scanner input is in-process only. A client performing the existing
filesystem fallback against an older relay still uses that host's legacy root,
without consulting the client's environment or disk. A new relay publishes the
same session schema to old and new clients; relocated conversations become visible.
No capability is needed to interpret those ordinary existing session rows.
This covers the relay process's inherited environment. A profile or root set only
inside an individual terminal is not automatically part of that environment. It
does not prove resolution of the original iOS timing report #18663.
Run the actual persistence-to-sidecar proof with a read-only OMP checkout:
```sh
ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-relay-root-smoke.mjs /path/to/oh-my-pi
```
It bundles the production service entry, launches it under native Node using the
production environment filter, and sends the real init/list IPC messages. OMP's
actual SessionManager writes default and named-profile XDG conversations in
isolated home/config/data directories while a legacy directory coexists. Both are
found by exact UUID and path. An invalid profile yields no OMP sessions. No model
calls or visible app windows are used. This local sidecar proof is not a live SSH
connection or Windows/Linux runtime validation.
@@ -0,0 +1,106 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform'
import { scanRemoteAiVaultSessions } from './remote-session-scanner'
import { MemoryRemoteProvider, jsonLines } from './remote-session-scanner-test-fixtures'
const home = '/home/ada'
const legacy = `${home}/.omp/agent/sessions`
const relocated = '/data/omp/profiles/work/sessions'
const platform = getRemoteHostPlatform('linux-x64')
function fixture() {
const provider = new MemoryRemoteProvider()
for (const [root, id] of [
[legacy, 'legacy'],
[relocated, 'relocated']
]) {
provider.addFile(
`${root}/folder/${id}.jsonl`,
jsonLines([
{ type: 'session', id, cwd: '/folder workspace', timestamp: '2026-09-14T10:00:00Z' },
{ type: 'message', message: { role: 'user', content: id } }
]),
70
)
}
return provider
}
afterEach(() => vi.unstubAllEnvs())
describe('host-resolved OMP remote scan roots', () => {
it('scans the host-selected root once instead of the coexisting legacy store', async () => {
const provider = fixture()
const result = await scanRemoteAiVaultSessions({
provider,
remoteHome: home,
hostPlatform: platform,
executionHostId: 'ssh:box',
ompSessionsDir: relocated
})
expect(result.sessions.map((row) => row.sessionId)).toEqual(['relocated'])
expect(result.sessions[0]?.executionHostId).toBe('ssh:box')
expect(provider.readDirPaths.filter((path) => path === relocated)).toHaveLength(1)
expect(provider.readDirPaths).not.toContain(legacy)
})
it('does not fall back when the host refuses an unsafe root', async () => {
const provider = fixture()
const result = await scanRemoteAiVaultSessions({
provider,
remoteHome: home,
hostPlatform: platform,
executionHostId: 'ssh:box',
ompSessionsDir: ''
})
expect(result.sessions).toEqual([])
expect(provider.readDirPaths).not.toContain(legacy)
expect(provider.readDirPaths).not.toContain('')
})
it('uses a Windows host root verbatim on a different client platform', async () => {
const provider = new MemoryRemoteProvider()
const root = 'D:\\OMP data\\profiles\\work\\sessions'
const file = `${root}\\folder\\session.jsonl`
provider.addFile(
file,
jsonLines([
{
type: 'session',
id: 'windows',
cwd: 'D:\\folder workspace',
timestamp: '2026-09-14T10:00:00Z'
},
{ type: 'message', message: { role: 'user', content: 'Windows host' } }
]),
80
)
const result = await scanRemoteAiVaultSessions({
provider,
remoteHome: 'C:\\Users\\ada',
hostPlatform: getRemoteHostPlatform('win32-x64'),
executionHostId: 'ssh:windows',
ompSessionsDir: root
})
expect(result.sessions).toHaveLength(1)
expect(result.sessions[0]).toMatchObject({
sessionId: 'windows',
executionHostId: 'ssh:windows',
executionHostPlatform: 'win32',
cwd: 'D:\\folder workspace'
})
expect(provider.readDirPaths).toContain(root.replaceAll('\\', '/'))
})
it('keeps legacy fallback scans independent of the client environment', async () => {
vi.stubEnv('OMP_CODING_AGENT_DIR', relocated)
vi.stubEnv('OMP_PROFILE', 'work')
const result = await scanRemoteAiVaultSessions({
provider: fixture(),
remoteHome: home,
hostPlatform: platform,
executionHostId: 'ssh:box'
})
expect(result.sessions.map((row) => row.sessionId)).toEqual(['legacy'])
})
})
@@ -17,7 +17,6 @@ import { parseHermesSessionContent } from './session-scanner-hermes-parser'
import { partitionSubagentTranscriptPaths } from './session-scanner-subagent-transcripts'
import { partitionOmpSubagentTranscriptPaths } from './session-scanner-omp-subagent-transcripts'
import type { FileWithMtime } from './session-scanner-types'
import { normalizeAgentSessionsDir } from './session-scanner-values'
import { remoteCodexIndexedTitleReader } from './remote-session-scanner-codex-index'
import { remoteClineSource } from './remote-session-scanner-cline-source'
import type {
@@ -26,6 +25,9 @@ import type {
RemoteSessionSource
} from './remote-session-scanner-types'
const PI_SESSIONS_SEGMENTS = ['.pi', 'agent', 'sessions']
const OMP_SESSIONS_SEGMENTS = ['.omp', 'agent', 'sessions']
type RemoteContentParser<T = string> = (
file: FileWithMtime,
content: T,
@@ -37,7 +39,8 @@ type RemoteContentParser<T = string> = (
export function remoteSessionSources(
remoteHome: string,
hostPlatform: RemoteHostPlatform
hostPlatform: RemoteHostPlatform,
ompSessionsDir?: string
): RemoteSessionSource[] {
return [
...remoteCodexSources(remoteHome, hostPlatform),
@@ -97,14 +100,16 @@ export function remoteSessionSources(
['.json'],
parseDevinSessionContent
),
jsonlSource('pi', remoteHome, hostPlatform, remotePiSessionsSegments(), piParser),
{
...jsonlSource('omp', remoteHome, hostPlatform, remoteOmpSessionsSegments(), ompParser),
// Same posture as Claude above: OMP stores task-subagent transcripts in
// the session's same-named artifact dir; the walk supplies counts and the
// partition keeps the children out of the top-level list (#9330).
partitionSubagentTranscripts: partitionOmpSubagentTranscriptPaths
},
jsonlSource('pi', remoteHome, hostPlatform, PI_SESSIONS_SEGMENTS, piParser),
...(ompSessionsDir === ''
? []
: [
{
...jsonlSource('omp', remoteHome, hostPlatform, OMP_SESSIONS_SEGMENTS, ompParser),
...(ompSessionsDir === undefined ? {} : { rootDir: ompSessionsDir }),
partitionSubagentTranscripts: partitionOmpSubagentTranscriptPaths
}
]),
jsonlSource(
'prime-agent',
remoteHome,
@@ -311,14 +316,6 @@ function remotePathSegments(path: string): string[] {
return path.replace(/\\/g, '/').split('/').filter(Boolean)
}
function remotePiSessionsSegments(): string[] {
return normalizeAgentSessionsDir('/.pi/agent/sessions', '.pi').split('/').filter(Boolean)
}
function remoteOmpSessionsSegments(): string[] {
return normalizeAgentSessionsDir('/.omp/agent/sessions', '.omp').split('/').filter(Boolean)
}
// Why: remote roots are posix regardless of the client platform, so these stay literal
// rather than round-tripping through a local-platform path join that would emit
// backslashes on a Windows client and collapse into a single bogus segment.
+3 -1
View File
@@ -50,6 +50,8 @@ export async function scanRemoteAiVaultSessions(args: {
provider: RemoteSessionFilesystemProvider
executionHostId: ExecutionHostId
remoteHome: string
// Host-resolved only; omission preserves legacy client-side fallback discovery.
ompSessionsDir?: string
hostPlatform: RemoteHostPlatform
limit?: number
unlimited?: boolean
@@ -85,7 +87,7 @@ export async function scanRemoteAiVaultSessions(args: {
const candidates = dedupeCodexRolloutFileAliases(
(
await mapRemoteScanBatches(
remoteSessionSources(args.remoteHome, args.hostPlatform),
remoteSessionSources(args.remoteHome, args.hostPlatform, args.ompSessionsDir),
REMOTE_SCAN_CONCURRENCY,
(source) => discoverRemoteSourceCandidates({ source, context, issues }),
args.signal
@@ -118,9 +118,7 @@ describe('buildRelayAiVaultServiceEnv', () => {
expect(env.HOME).toBe('/home/ada')
})
// The sidecar takes remoteHome and hostPlatform from its init message, so an
// agent-home override on the remote host is not part of how it finds roots.
it('withholds the agent-home variables the desktop child needs', () => {
it('withholds unrelated agent-home variables', () => {
const env = buildRelayAiVaultServiceEnv(
{ CODEX_HOME: '/remote/.codex', PATH: '/usr/bin' },
'linux'
@@ -135,7 +133,7 @@ describe('buildRelayAiVaultServiceEnv', () => {
})
})
it('carries OMP root/profile inputs only to the desktop service, retaining empty canonical profile', () => {
it('carries OMP root/profile inputs to both services, retaining empty canonical profile', () => {
const roots = {
OMP_PROFILE: '',
PI_PROFILE: 'work',
@@ -144,5 +142,30 @@ it('carries OMP root/profile inputs only to the desktop service, retaining empty
XDG_DATA_HOME: '/home/dev/data'
}
expect(buildAiVaultServiceEnv(roots, 'linux')).toEqual({ ...roots, ELECTRON_RUN_AS_NODE: '1' })
expect(buildRelayAiVaultServiceEnv(roots, 'linux')).toEqual({})
expect(buildRelayAiVaultServiceEnv(roots, 'linux')).toEqual(roots)
})
it('carries Windows OMP roots case-insensitively without forwarding other agent roots', () => {
expect(
buildRelayAiVaultServiceEnv(
{
omp_profile: 'work',
omp_coding_agent_dir: 'D:\\omp',
pi_profile: 'old',
pi_config_dir: '.custom',
pi_coding_agent_dir: 'D:\\inherited',
xdg_data_home: 'D:\\data',
codex_home: 'D:\\codex',
AWS_SECRET_ACCESS_KEY: 'secret'
},
'win32'
)
).toEqual({
OMP_PROFILE: 'work',
OMP_CODING_AGENT_DIR: 'D:\\omp',
PI_PROFILE: 'old',
PI_CONFIG_DIR: '.custom',
PI_CODING_AGENT_DIR: 'D:\\inherited',
XDG_DATA_HOME: 'D:\\data'
})
})
@@ -30,6 +30,15 @@ export const RUNTIME_ENV_ALLOWLIST = [
'NUMBER_OF_PROCESSORS'
] as const
const OMP_ROOT_ENV_ALLOWLIST = [
'OMP_CODING_AGENT_DIR',
'OMP_PROFILE',
'PI_CODING_AGENT_DIR',
'PI_CONFIG_DIR',
'PI_PROFILE',
'XDG_DATA_HOME'
] as const
// Why: the desktop child resolves agent roots from its own environment, so
// dropping one hides every session of a user who relocated that agent's home.
const AGENT_ROOT_ENV_ALLOWLIST = [
@@ -39,19 +48,12 @@ const AGENT_ROOT_ENV_ALLOWLIST = [
'DEVIN_HOME',
'GROK_HOME',
'KIMI_CODE_HOME',
'OMP_CODING_AGENT_DIR',
'OMP_PROFILE',
'OPENCLAW_STATE_DIR',
'OPENCODE_DB',
'PI_CODING_AGENT_DIR',
'PI_CONFIG_DIR',
'PI_PROFILE',
'PRIME_AGENT_CODING_AGENT_DIR',
'PRIME_AGENT_CODING_AGENT_SESSION_DIR',
'PRIME_AGENT_SESSION_DIR',
// Why: OpenCode and migrated OMP session stores use the XDG data dir,
// so this one is an agent root here rather than generic runtime state.
'XDG_DATA_HOME'
...OMP_ROOT_ENV_ALLOWLIST
] as const
export function pickAllowedEnv(
@@ -94,10 +96,10 @@ export function buildAiVaultServiceEnv(
return env
}
/** Relay: the sidecar takes every root from its init message, not the environment. */
/** Relay: resolve OMP storage from the execution host, never the client. */
export function buildRelayAiVaultServiceEnv(
baseEnv: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform
): NodeJS.ProcessEnv {
return pickAllowedEnv(RUNTIME_ENV_ALLOWLIST, baseEnv, platform)
return pickAllowedEnv([...RUNTIME_ENV_ALLOWLIST, ...OMP_ROOT_ENV_ALLOWLIST], baseEnv, platform)
}
+5
View File
@@ -1,3 +1,4 @@
import { resolveOmpSessionsDir } from '../main/ai-vault/omp-session-root'
import { LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
import { scanRemoteAiVaultSessions } from '../main/ai-vault/remote-session-scanner'
import { readAiVaultSessionTitlesFromFiles } from '../main/ai-vault/session-title-file-reader'
@@ -50,6 +51,10 @@ async function execute(request: RelayAiVaultServiceRequest): Promise<void> {
provider,
executionHostId: LOCAL_EXECUTION_HOST_ID,
remoteHome: init.remoteHome,
ompSessionsDir: resolveOmpSessionsDir({
homeDir: init.remoteHome,
platform: init.hostPlatform.os
}),
hostPlatform: init.hostPlatform,
limit: request.params.limit,
unlimited: request.params.unlimited,
+145
View File
@@ -0,0 +1,145 @@
import assert from 'node:assert/strict'
import { mkdtemp, mkdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import { spawnProcess } from '../../src/shared/child-process/run-process.ts'
import { buildRelayAiVaultServiceEnv } from '../../src/main/ai-vault/session-scanner-service-env.ts'
import { getRemoteHostPlatform } from '../../src/main/ssh/ssh-remote-platform.ts'
assert.ok(process.argv[2], 'Pass a read-only OMP checkout')
const node = Bun.which('node')
assert.ok(node, 'A native Node executable is required')
const root = fileURLToPath(new URL('../../', import.meta.url))
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-relay-root-'))
const home = join(scratch, 'home')
const xdg = join(scratch, 'data')
Object.assign(process.env, {
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(scratch, 'config'),
XDG_DATA_HOME: xdg,
XDG_STATE_HOME: join(scratch, 'state'),
XDG_CACHE_HOME: join(scratch, 'cache')
})
for (const key of [
'OMP_CODING_AGENT_DIR',
'PI_CODING_AGENT_DIR',
'OMP_PROFILE',
'PI_PROFILE',
'PI_CONFIG_DIR',
'PI_CONFIG_FILES'
]) {
delete process.env[key]
}
const source = (path) => pathToFileURL(join(resolve(process.argv[2]), path)).href
const managers = []
const entry = join(scratch, 'relay-ai-vault-service.cjs')
async function scan(profile, env = process.env) {
const child = spawnProcess({
program: node,
args: [entry],
cwd: scratch,
env: { ...buildRelayAiVaultServiceEnv(env), ORCA_BACKGROUND_LAUNCH: '1' },
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
})
let stderr = ''
child.stderr.on('data', (data) => {
stderr = (stderr + data).slice(-16000)
})
const exited = new Promise((resolveExit) => child.once('exit', resolveExit))
let timer
try {
const result = new Promise((resolveResult, reject) => {
timer = setTimeout(() => reject(new Error(`Relay scan timed out: ${stderr}`)), 15000)
child.once('error', reject)
child.once('exit', (code) => reject(new Error(`Relay exited ${code}: ${stderr}`)))
child.on('message', (message) => {
if (message.type === 'ready') {
child.send({ type: 'request', id: 1, operation: 'list', params: { limit: 20 } })
} else if (message.type === 'error') {
reject(new Error(message.message))
} else if (message.type === 'result') {
resolveResult(message.value)
}
})
})
child.send({
type: 'init',
protocol: 1,
remoteHome: home,
hostPlatform: getRemoteHostPlatform(`${process.platform}-${process.arch}`)
})
const value = await result
assert.equal(value.issues.length, 0, JSON.stringify(value.issues))
const sessions = value.sessions.filter((session) => session.agent === 'omp')
if (profile === 'invalid') {
assert.deepEqual(sessions, [])
} else {
const manager = managers.at(-1)
assert.equal(sessions.length, 1)
assert.equal(sessions[0].sessionId, manager.getSessionId())
assert.equal(sessions[0].filePath, manager.getSessionFile())
assert.equal(sessions[0].cwd, join(scratch, 'folder workspace'))
}
return { profile: profile || 'default', sessions: sessions.length, actualNodeSidecar: true }
} finally {
clearTimeout(timer)
child.kill()
await exited
}
}
try {
await mkdir(join(home, '.omp', 'agent', 'sessions'), { recursive: true })
await mkdir(join(xdg, 'omp', 'profiles', 'work'), { recursive: true })
await build({
entryPoints: [join(root, 'src/relay/ai-vault-service-entry.ts')],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: entry,
external: ['electron'],
define: { 'process.env.NODE_ENV': '"production"' }
})
const upstream = await import(source('packages/utils/src/dirs.ts'))
const { SessionManager } = await import(
source('packages/coding-agent/src/session/session-manager.ts')
)
const results = []
for (const profile of ['', 'work']) {
process.env.OMP_PROFILE = profile
upstream.__resetDirsFromEnvForTests()
const cwd = join(scratch, 'folder workspace')
await mkdir(cwd, { recursive: true })
const manager = SessionManager.create(cwd)
managers.push(manager)
manager.appendMessage({
role: 'user',
content: 'OMP relay history proof',
timestamp: Date.now()
})
await manager.ensureOnDisk()
await manager.flush()
const expected = join(xdg, 'omp', ...(profile ? ['profiles', profile] : []), 'sessions')
assert.ok(manager.getSessionFile().startsWith(expected))
results.push(await scan(profile))
}
results.push(await scan('invalid', { ...process.env, OMP_PROFILE: '../invalid' }))
console.log(
JSON.stringify({
results,
actualOmpPersistence: true,
legacyDirectoryCoexists: true,
modelCalls: 0
})
)
} finally {
for (const manager of managers) {
await manager.close()
}
await rm(scratch, { recursive: true, force: true })
}