Show OMP sessions in AI Vault history

This commit is contained in:
brennanb2025
2026-06-23 20:42:40 -07:00
parent e03a1cf769
commit 780100c9fa
12 changed files with 252 additions and 87 deletions
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { buildAiVaultResumeCommand } from '../../shared/ai-vault-types'
describe('buildAiVaultResumeCommand', () => {
it('wraps Windows cwd changes in cmd so PowerShell and cmd launch the same resume command', () => {
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: 'C:\\Users\\Ada Lovelace\\repo',
platform: 'win32'
})
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && codex resume ""session-1"""')
expect(
buildAiVaultResumeCommand({
agent: 'omp',
sessionId: 'omp session',
cwd: 'C:\\Users\\Ada Lovelace\\repo',
platform: 'win32'
})
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && omp --resume ""omp session"""')
})
it('carries non-default Codex homes in copied resume commands', () => {
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: '/repo/app',
platform: 'darwin',
codexHome: '/Users/ada/Library/Application Support/Orca/codex-runtime-home/home'
})
).toBe(
"cd '/repo/app' && CODEX_HOME='/Users/ada/Library/Application Support/Orca/codex-runtime-home/home' codex resume 'session-1'"
)
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: 'C:\\Users\\Ada Lovelace\\repo',
platform: 'win32',
codexHome: 'C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home'
})
).toBe(
'cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && set ""CODEX_HOME=C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home"" && codex resume ""session-1"""'
)
})
})
@@ -67,6 +67,8 @@ export async function parseAgentSessionFile(
return parseMessageGraphSessionFile('openclaw', candidate.file, platform)
case 'pi':
return parseMessageGraphSessionFile('pi', candidate.file, platform)
case 'omp':
return parseMessageGraphSessionFile('omp', candidate.file, platform)
case 'droid':
return parseDroidSessionFile(candidate.file, platform)
case 'devin':
@@ -117,6 +117,7 @@ describe('scanAiVaultSessions Codex worker sessions', () => {
openclawStateDir: join(root, 'openclaw-state'),
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions'),
@@ -155,7 +155,7 @@ export function rovoPartsText(parts: unknown[], role: 'user' | 'assistant'): str
}
export async function parseMessageGraphSessionFile(
agent: 'openclaw' | 'pi',
agent: 'openclaw' | 'pi' | 'omp',
file: FileWithMtime,
platform: NodeJS.Platform = process.platform
): Promise<AiVaultSession | null> {
@@ -181,10 +181,12 @@ export async function parseMessageGraphSessionFile(
accumulator.sessionId = sessionId
}
accumulator.cwd = extractString(record.cwd) ?? accumulator.cwd
accumulator.title ??= normalizeTitleText(extractString(record.title) ?? '')
continue
}
if (record.type === 'model_change') {
accumulator.model = extractString(record.modelId) ?? accumulator.model
accumulator.model =
extractString(record.model) ?? extractString(record.modelId) ?? accumulator.model
continue
}
if (record.type !== 'message') {
@@ -0,0 +1,85 @@
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { scanAiVaultSessions } from './session-scanner'
let tempRoots: string[] = []
afterEach(async () => {
vi.restoreAllMocks()
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
tempRoots = []
})
function isolatedGrokScanRoots(root: string) {
return {
claudeProjectsDir: join(root, 'claude-projects'),
codexSessionsDir: join(root, 'codex-sessions'),
geminiSessionsDir: join(root, 'gemini-sessions'),
copilotSessionsDir: join(root, 'copilot-sessions'),
cursorProjectsDir: join(root, 'cursor-projects'),
opencodeStorageDir: join(root, 'opencode-storage'),
opencodeDbPaths: [] as readonly string[],
grokSessionsDir: join(root, 'grok-sessions'),
devinTranscriptsDir: join(root, 'devin-transcripts'),
hermesSessionsDir: join(root, 'hermes-sessions'),
rovoSessionsDir: join(root, 'rovo-sessions'),
openclawStateDir: join(root, 'openclaw-state'),
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions')
}
}
function jsonLines(records: unknown[]): string {
return records.map((record) => JSON.stringify(record)).join('\n')
}
describe('scanAiVaultSessions Grok envelopes', () => {
it('strips newline-heavy Grok user_query envelopes without regex matching', async () => {
const matchSpy = vi.spyOn(String.prototype, 'match')
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-grok-large-'))
tempRoots.push(root)
const roots = isolatedGrokScanRoots(root)
const sessionDir = join(roots.grokSessionsDir, encodeURIComponent('/tmp/grok'), 'large-session')
const requestText = 'Grok large title\n'.repeat(300)
await mkdir(sessionDir, { recursive: true })
await writeFile(
join(sessionDir, 'summary.json'),
JSON.stringify({
info: { id: 'large-session', cwd: '/tmp/grok' },
created_at: '2026-05-01T10:04:00.000Z'
})
)
await writeFile(
join(sessionDir, 'chat_history.jsonl'),
jsonLines([
{
type: 'user',
content: `<USER_INFO>context</USER_INFO><USER_QUERY>\n${requestText}</USER_QUERY>`
}
])
)
const result = await scanAiVaultSessions({
...roots,
platform: 'darwin',
limit: 5
})
expect(result.issues).toEqual([])
expect(result.sessions[0]?.title).toContain('Grok large title')
expect(result.sessions[0]?.title).not.toContain('USER_QUERY')
const usedGrokWrapperMatch = matchSpy.mock.calls.some(
([pattern]) =>
pattern instanceof RegExp &&
pattern.source.includes('<user_query>') &&
pattern.source.includes('[\\s\\S]')
)
expect(usedGrokWrapperMatch).toBe(false)
})
})
@@ -34,6 +34,7 @@ function isolatedScanRoots(root: string) {
openclawStateDir: join(root, 'openclaw-state'),
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions')
@@ -28,6 +28,7 @@ const OPENCLAW_STATE_DIR = process.env.OPENCLAW_STATE_DIR?.trim() || join(homedi
const PI_SESSIONS_DIR = normalizePiSessionsDir(
process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent', 'sessions')
)
const OMP_SESSIONS_DIR = normalizePiSessionsDir(join(homedir(), '.omp', 'agent', 'sessions'))
// Why: Devin ATIF transcripts are stored under <DEVIN_HOME>/transcripts.
const DEVIN_TRANSCRIPTS_DIR = join(
process.env.DEVIN_HOME?.trim() || join(homedir(), '.local', 'share', 'devin', 'cli'),
@@ -114,7 +115,8 @@ function standardDiscoveries(
...devinDiscoveries(options, wslHomeDirs, limit, issues),
...hermesDiscoveries(options, wslHomeDirs, limit, issues),
...rovoDiscoveries(options, wslHomeDirs, limit, issues),
...piDiscoveries(options, wslHomeDirs, limit, issues)
...piDiscoveries(options, wslHomeDirs, limit, issues),
...ompDiscoveries(options, wslHomeDirs, limit, issues)
]
}
@@ -234,6 +236,21 @@ function piDiscoveries(
)
}
function ompDiscoveries(
options: AiVaultScanOptions,
wslHomeDirs: readonly string[],
limit: number,
issues: AiVaultScanIssue[]
): Promise<SessionFileDiscovery>[] {
return sessionRootDirs(options.ompSessionsDir ?? OMP_SESSIONS_DIR, wslHomeDirs, [
'.omp',
'agent',
'sessions'
]).map((rootDir) =>
discoverFiles({ rootDir, limit, agent: 'omp', issues, extensions: ['.jsonl'] })
)
}
function openClawDiscovery(
options: AiVaultScanOptions,
wslHomeDirs: readonly string[],
@@ -24,6 +24,7 @@ export type AiVaultScanOptions = {
openclawStateDir?: string
openclawLegacyStateDir?: string
piSessionsDir?: string
ompSessionsDir?: string
droidSessionsDir?: string
droidProjectsDir?: string
kimiSessionsDir?: string
+48 -83
View File
@@ -1,14 +1,13 @@
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AI_VAULT_AGENTS, buildAiVaultResumeCommand } from '../../shared/ai-vault-types'
import { afterEach, describe, expect, it } from 'vitest'
import { AI_VAULT_AGENTS } from '../../shared/ai-vault-types'
import { scanAiVaultSessions } from './session-scanner'
let tempRoots: string[] = []
afterEach(async () => {
vi.restoreAllMocks()
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
tempRoots = []
})
@@ -31,6 +30,7 @@ function isolatedScanRoots(root: string) {
openclawStateDir: join(root, 'openclaw-state'),
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions')
@@ -611,6 +611,42 @@ describe('scanAiVaultSessions', () => {
])
)
await mkdir(join(roots.ompSessionsDir, encodeURIComponent('/tmp/omp')), { recursive: true })
await writeFile(
join(roots.ompSessionsDir, encodeURIComponent('/tmp/omp'), '2026-05-01_omp-session.jsonl'),
jsonLines([
{
type: 'session',
id: 'omp-session',
title: 'OMP header title',
timestamp: '2026-05-01T10:08:30.000Z',
cwd: '/tmp/omp'
},
{
type: 'model_change',
timestamp: '2026-05-01T10:08:31.000Z',
model: 'omp-model'
},
{
type: 'message',
timestamp: '2026-05-01T10:08:32.000Z',
message: {
role: 'user',
content: [{ type: 'text', text: 'OMP first user fallback title' }]
}
},
{
type: 'message',
timestamp: '2026-05-01T10:08:33.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'Done' }],
usage: { input: 3, output: 5, cacheRead: 7, cacheWrite: 11 }
}
}
])
)
await mkdir(roots.devinTranscriptsDir, { recursive: true })
await writeFile(
join(roots.devinTranscriptsDir, 'devin-session.json'),
@@ -746,92 +782,21 @@ describe('scanAiVaultSessions', () => {
"cd '/tmp/openclaw' && openclaw --resume 'openclaw-session'"
)
expect(commandByAgent.get('pi')).toBe("cd '/tmp/pi' && pi --session 'pi-session'")
expect(commandByAgent.get('omp')).toBe("cd '/tmp/omp' && omp --resume 'omp-session'")
expect(commandByAgent.get('devin')).toBe("cd '/tmp/devin' && devin --resume 'devin-session'")
expect(commandByAgent.get('droid')).toBe("cd '/tmp/droid' && droid --resume 'droid-session'")
expect(commandByAgent.get('kimi')).toBe(
"cd '/tmp/kimi' && kimi --session 'session_kimi-session'"
)
})
it('strips newline-heavy Grok user_query envelopes without regex matching', async () => {
const matchSpy = vi.spyOn(String.prototype, 'match')
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-grok-large-'))
tempRoots.push(root)
const roots = isolatedScanRoots(root)
const sessionDir = join(roots.grokSessionsDir, encodeURIComponent('/tmp/grok'), 'large-session')
const requestText = 'Grok large title\n'.repeat(300)
await mkdir(sessionDir, { recursive: true })
await writeFile(
join(sessionDir, 'summary.json'),
JSON.stringify({
info: { id: 'large-session', cwd: '/tmp/grok' },
created_at: '2026-05-01T10:04:00.000Z'
})
)
await writeFile(
join(sessionDir, 'chat_history.jsonl'),
jsonLines([
{
type: 'user',
content: `<USER_INFO>context</USER_INFO><USER_QUERY>\n${requestText}</USER_QUERY>`
}
])
)
const result = await scanAiVaultSessions({
...roots,
platform: 'darwin',
limit: 5
const omp = result.sessions.find((session) => session.agent === 'omp')
expect(omp).toMatchObject({
sessionId: 'omp-session',
title: 'OMP header title',
cwd: '/tmp/omp',
model: 'omp-model',
messageCount: 2,
totalTokens: 26
})
expect(result.issues).toEqual([])
expect(result.sessions[0]?.title).toContain('Grok large title')
expect(result.sessions[0]?.title).not.toContain('USER_QUERY')
const usedGrokWrapperMatch = matchSpy.mock.calls.some(
([pattern]) =>
pattern instanceof RegExp &&
pattern.source.includes('<user_query>') &&
pattern.source.includes('[\\s\\S]')
)
expect(usedGrokWrapperMatch).toBe(false)
})
})
describe('buildAiVaultResumeCommand', () => {
it('wraps Windows cwd changes in cmd so PowerShell and cmd launch the same resume command', () => {
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: 'C:\\Users\\Ada Lovelace\\repo',
platform: 'win32'
})
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && codex resume ""session-1"""')
})
it('carries non-default Codex homes in copied resume commands', () => {
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: '/repo/app',
platform: 'darwin',
codexHome: '/Users/ada/Library/Application Support/Orca/codex-runtime-home/home'
})
).toBe(
"cd '/repo/app' && CODEX_HOME='/Users/ada/Library/Application Support/Orca/codex-runtime-home/home' codex resume 'session-1'"
)
expect(
buildAiVaultResumeCommand({
agent: 'codex',
sessionId: 'session-1',
cwd: 'C:\\Users\\Ada Lovelace\\repo',
platform: 'win32',
codexHome: 'C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home'
})
).toBe(
'cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && set ""CODEX_HOME=C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home"" && codex resume ""session-1"""'
)
})
})
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
import { AI_VAULT_AGENTS, type AiVaultSession } from '../../../../shared/ai-vault-types'
import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config'
import {
AI_VAULT_SESSION_FILTER_QUERY_MAX_BYTES,
deriveAiVaultWorkspaceScopePaths,
@@ -30,6 +31,11 @@ const baseSession: AiVaultSession = {
}
describe('filterAiVaultSessions', () => {
it('offers OMP in the agent filter because OMP is a supported TUI agent', () => {
expect(TUI_AGENT_CONFIG.omp).toBeDefined()
expect(AI_VAULT_AGENTS).toContain('omp')
})
it('filters by workspace, agent, plain terms, repo: and path: operators', () => {
const sessions: AiVaultSession[] = [
baseSession,
@@ -9,6 +9,23 @@ vi.mock('@/lib/new-workspace', () => ({
CLIENT_PLATFORM: 'win32'
}))
vi.mock('@/lib/local-preflight-context', () => ({
getLocalProjectExecutionRuntimeContext: (
state: Pick<AppState, 'projects'>,
_worktreeId: string | null | undefined,
platform: NodeJS.Platform
) => {
if (platform !== 'win32') {
return undefined
}
const preference = state.projects[0]?.localWindowsRuntimePreference
if (preference?.kind !== 'wsl') {
return undefined
}
return { status: 'resolved', runtime: preference }
}
}))
type RuntimePreference = { kind: 'windows-host' } | { kind: 'wsl'; distro: string }
function makeState(args: {
@@ -63,6 +80,19 @@ describe('ai vault resume command runtime', () => {
}
})
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\alice\\repo"" && claude --resume ""session one"""')
expect(
buildAiVaultResumeCommandForWorktree({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'omp',
sessionId: 'omp session',
cwd: 'C:\\Users\\alice\\repo',
codexHome: null
}
})
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\alice\\repo"" && omp --resume ""omp session"""')
})
it('uses POSIX command wrapping for Windows-path projects forced to WSL', () => {
+5
View File
@@ -6,6 +6,7 @@ export const AI_VAULT_AGENTS = [
'codex',
'hermes',
'pi',
'omp',
'cursor',
'gemini',
'rovo',
@@ -28,6 +29,7 @@ export const AI_VAULT_AGENT_LABELS = {
codex: 'Codex',
hermes: 'Hermes',
pi: 'Pi',
omp: 'OMP',
cursor: 'Cursor',
gemini: 'Gemini',
rovo: 'Rovo Dev',
@@ -147,6 +149,9 @@ function buildAgentResumeInvocation(
return `${baseCommand} --session ${sessionArg}`
case 'copilot':
return `${baseCommand} --resume=${sessionArg}`
// Why: OMP stores Pi-shaped transcripts, but its CLI resumes with
// `omp --resume <id>` rather than Pi's `--session` flag.
case 'omp':
case 'claude':
case 'cursor':
case 'gemini':