Ensure OMP SQLite databases are source-backed in agent overlays (#5603)

OMP creates SQLite databases like agent.db lazily under the agent PTY
directory. Proactively pre-create and link these database files (plus WAL/SHM
sidecars on Windows) to the user's persistent store so that credentials and
history are not lost in disposable overlays.

- Implement `mirrorOmpPersistentSqliteFiles` to handle linking `agent.db`
- Support both Unix symlinks and Windows hardlinks for writable state
- Extract prefill and titlebar extension source code into separate files
This commit is contained in:
Jinjing
2026-06-17 10:21:57 -07:00
committed by GitHub
parent 913c587162
commit 3518e13cff
9 changed files with 336 additions and 116 deletions
+38
View File
@@ -0,0 +1,38 @@
import type { PiAgentKind } from '../../shared/pi-agent-kind'
export const ORCA_PI_PREFILL_EXTENSION_FILE = 'orca-prefill.ts'
// Why: prefill-without-submit needs an env-var the bundled `orca-prefill.ts`
// extension can read on session_start. Each kind owns its own variable so an
// OMP PTY never honors a Pi draft (or vice versa).
const PREFILL_ENV_VAR_BY_KIND: Record<PiAgentKind, string> = {
pi: 'ORCA_PI_PREFILL',
omp: 'ORCA_OMP_PREFILL'
}
/** Pi's prefill env var. Exported for callers that need the literal name
* (renderer draft-launch plan builder, tests). OMP callers should read
* `ORCA_OMP_PREFILL_ENV_VAR` instead. */
export const ORCA_PI_PREFILL_ENV_VAR = PREFILL_ENV_VAR_BY_KIND.pi
/** OMP's prefill env var. Mirrors `ORCA_PI_PREFILL_ENV_VAR` for OMP launches
* so renderer plans and shell-ready restore lines can stay agent-scoped. */
export const ORCA_OMP_PREFILL_ENV_VAR = PREFILL_ENV_VAR_BY_KIND.omp
export function getPiPrefillExtensionSource(kind: PiAgentKind): string {
const envVar = PREFILL_ENV_VAR_BY_KIND[kind]
return [
'export default function (pi) {',
" pi.on('session_start', async (event, ctx) => {",
" if (event.reason !== 'startup') return",
` const prefill = process.env.${envVar}`,
' if (!prefill) return',
` delete process.env.${envVar}`,
' try {',
' ctx.ui.setEditorText(prefill)',
' } catch {}',
' })',
'}',
''
].join('\n')
}
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
@@ -187,6 +188,25 @@ describe('PiTitlebarExtensionService', () => {
expectPiHomeIntact()
})
it('source-backs OMP agent.db before OMP lazily creates it', () => {
const svc = new PiTitlebarExtensionService()
const env = svc.buildPtyEnv('pty-omp-sqlite', piHome, 'omp')
const sourcePath = join(piHome, 'agent.db')
const overlayPath = join(env.PI_CODING_AGENT_DIR!, 'agent.db')
const content = 'agent.db credentials'
expect(existsSync(sourcePath)).toBe(true)
expect(existsSync(overlayPath)).toBe(true)
expect(existsSync(join(piHome, 'history.db'))).toBe(false)
writeFileSync(overlayPath, content)
expect(readFileSync(sourcePath, 'utf-8')).toBe(content)
if (process.platform !== 'win32') {
expect(lstatSync(overlayPath).isSymbolicLink()).toBe(true)
}
})
it('rebuilding an overlay for the same ptyId does not corrupt the user Pi dir', () => {
const svc = new PiTitlebarExtensionService()
svc.buildPtyEnv('pty-3', piHome, 'pi')
+27 -110
View File
@@ -15,12 +15,22 @@ import {
ORCA_PI_AGENT_STATUS_EXTENSION_FILE,
getPiAgentStatusExtensionSource
} from './agent-status-extension-source'
import {
ORCA_PI_PREFILL_EXTENSION_FILE,
getPiPrefillExtensionSource
} from './prefill-extension-source'
export { ORCA_OMP_PREFILL_ENV_VAR, ORCA_PI_PREFILL_ENV_VAR } from './prefill-extension-source'
import { ORCA_PI_EXTENSION_FILE, getPiTitlebarExtensionSource } from './titlebar-extension-source'
import {
isSafeDescendCandidate as sharedIsSafeDescendCandidate,
mirrorEntry,
safeRemoveOverlay,
safeRemoveTree
} from '../pty/overlay-mirror'
import {
isOmpPersistentSqliteEntry,
mirrorOmpPersistentSqliteFiles
} from '../pty/omp-sqlite-overlay'
import { mergePiOverlayUiSettings } from '../../shared/pi-overlay-ui-settings'
import type { PiAgentKind } from '../../shared/pi-agent-kind'
@@ -30,8 +40,6 @@ import type { PiAgentKind } from '../../shared/pi-agent-kind'
// keeps holding after the helper moved to src/main/pty/overlay-mirror.ts.
export const isSafeDescendCandidate = sharedIsSafeDescendCandidate
const ORCA_PI_EXTENSION_FILE = 'orca-titlebar-spinner.ts'
const ORCA_PI_PREFILL_EXTENSION_FILE = 'orca-prefill.ts'
const PI_AGENT_SUBDIR = 'agent'
const PI_AGENT_SETTINGS_FILE = 'settings.json'
const PI_OVERLAY_MANIFEST_FILE = '.orca-pi-overlay-manifest.json'
@@ -59,112 +67,6 @@ const AGENT_HOME_DIR_NAME: Record<PiAgentKind, string> = {
omp: '.omp'
}
// Why: prefill-without-submit needs an env-var the bundled `orca-prefill.ts`
// extension can read on session_start. Each kind owns its own variable so an
// OMP PTY never honors a Pi draft (or vice versa) - the only state the two
// share is the binary-mandated `PI_CODING_AGENT_DIR` itself. Pi keeps its
// original name for back-compat with PTYs already in flight at upgrade time;
// OMP gets a parallel `ORCA_OMP_PREFILL`.
const PREFILL_ENV_VAR_BY_KIND: Record<PiAgentKind, string> = {
pi: 'ORCA_PI_PREFILL',
omp: 'ORCA_OMP_PREFILL'
}
/** Pi's prefill env var. Exported for callers that need the literal name
* (renderer draft-launch plan builder, tests). OMP callers should read
* `ORCA_OMP_PREFILL_ENV_VAR` instead. */
export const ORCA_PI_PREFILL_ENV_VAR = PREFILL_ENV_VAR_BY_KIND.pi
/** OMP's prefill env var. Mirrors `ORCA_PI_PREFILL_ENV_VAR` for OMP launches
* so renderer plans and shell-ready restore lines can stay agent-scoped. */
export const ORCA_OMP_PREFILL_ENV_VAR = PREFILL_ENV_VAR_BY_KIND.omp
// Why: pi/OMP both expose an `ExtensionAPI` to the default-exported factory.
// The extension reads its kind-specific env var on session_start and types
// the payload into the editor without submitting. The env var is consumed
// (deleted from process.env) so `/new` in the same session doesn't re-prefill.
// The extension API name is identical between pi and OMP because OMP keeps
// Pi's `@oh-my-pi/*` runtime packages.
function getPiPrefillExtensionSource(kind: PiAgentKind): string {
const envVar = PREFILL_ENV_VAR_BY_KIND[kind]
return [
'export default function (pi) {',
" pi.on('session_start', async (event, ctx) => {",
" if (event.reason !== 'startup') return",
` const prefill = process.env.${envVar}`,
' if (!prefill) return',
` delete process.env.${envVar}`,
' try {',
' ctx.ui.setEditorText(prefill)',
' } catch {}',
' })',
'}',
''
].join('\n')
}
function getPiTitlebarExtensionSource(): string {
return [
'const BRAILLE_FRAMES = [',
" '\\u280b',",
" '\\u2819',",
" '\\u2839',",
" '\\u2838',",
" '\\u283c',",
" '\\u2834',",
" '\\u2826',",
" '\\u2827',",
" '\\u2807',",
" '\\u280f'",
']',
'',
'function getBaseTitle(pi) {',
' const cwd = process.cwd().split(/[\\\\/]/).filter(Boolean).at(-1) || process.cwd()',
' const session = pi.getSessionName()',
' return session ? `\\u03c0 - ${session} - ${cwd}` : `\\u03c0 - ${cwd}`',
'}',
'',
'export default function (pi) {',
' let timer = null',
' let frameIndex = 0',
'',
' function stopAnimation(ctx) {',
' if (timer) {',
' clearInterval(timer)',
' timer = null',
' }',
' frameIndex = 0',
' ctx.ui.setTitle(getBaseTitle(pi))',
' }',
'',
' function startAnimation(ctx) {',
' stopAnimation(ctx)',
' timer = setInterval(() => {',
' const frame = BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]',
' const cwd = process.cwd().split(/[\\\\/]/).filter(Boolean).at(-1) || process.cwd()',
' const session = pi.getSessionName()',
' const title = session ? `${frame} \\u03c0 - ${session} - ${cwd}` : `${frame} \\u03c0 - ${cwd}`',
' ctx.ui.setTitle(title)',
' frameIndex++',
' }, 80)',
' }',
'',
" pi.on('agent_start', async (_event, ctx) => {",
' startAnimation(ctx)',
' })',
'',
" pi.on('agent_end', async (_event, ctx) => {",
' stopAnimation(ctx)',
' })',
'',
" pi.on('session_shutdown', async (_event, ctx) => {",
' stopAnimation(ctx)',
' })',
'}',
''
].join('\n')
}
function getDefaultPiAgentDir(kind: PiAgentKind): string {
return join(homedir(), AGENT_HOME_DIR_NAME[kind], PI_AGENT_SUBDIR)
}
@@ -234,13 +136,18 @@ export class PiTitlebarExtensionService {
}
}
private mirrorAgentDir(sourceAgentDir: string, overlayDir: string): void {
private mirrorAgentDir(sourceAgentDir: string, overlayDir: string, kind: PiAgentKind): void {
const previousManifest = this.readOverlayManifest(overlayDir)
this.clearManifestEntries(overlayDir, previousManifest)
const nextManifest: PiOverlayManifest = { topLevelEntries: [], extensionEntries: [] }
if (!existsSync(sourceAgentDir)) {
if (kind === 'omp') {
nextManifest.topLevelEntries.push(
...mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
)
}
this.writeOverlayManifest(overlayDir, nextManifest)
return
}
@@ -252,6 +159,10 @@ export class PiTitlebarExtensionService {
continue
}
if (kind === 'omp' && isOmpPersistentSqliteEntry(entry.name)) {
continue
}
if (entry.name === 'extensions') {
const isSymlink = entry.isSymbolicLink()
let isLinkPointingToDir = false
@@ -300,6 +211,12 @@ export class PiTitlebarExtensionService {
nextManifest.topLevelEntries.push(entry.name)
}
if (kind === 'omp') {
nextManifest.topLevelEntries.push(
...mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
)
}
this.writeOverlayManifest(overlayDir, nextManifest)
}
@@ -349,7 +266,7 @@ export class PiTitlebarExtensionService {
try {
mkdirSync(overlayDir, { recursive: true })
this.mirrorAgentDir(sourceAgentDir, overlayDir)
this.mirrorAgentDir(sourceAgentDir, overlayDir, kind)
this.writeOverlaySettings(sourceAgentDir, overlayDir)
const extensionsDir = join(overlayDir, 'extensions')
+63
View File
@@ -0,0 +1,63 @@
export const ORCA_PI_EXTENSION_FILE = 'orca-titlebar-spinner.ts'
export function getPiTitlebarExtensionSource(): string {
return [
'const BRAILLE_FRAMES = [',
" '\\u280b',",
" '\\u2819',",
" '\\u2839',",
" '\\u2838',",
" '\\u283c',",
" '\\u2834',",
" '\\u2826',",
" '\\u2827',",
" '\\u2807',",
" '\\u280f'",
']',
'',
'function getBaseTitle(pi) {',
' const cwd = process.cwd().split(/[\\\\/]/).filter(Boolean).at(-1) || process.cwd()',
' const session = pi.getSessionName()',
' return session ? `\\u03c0 - ${session} - ${cwd}` : `\\u03c0 - ${cwd}`',
'}',
'',
'export default function (pi) {',
' let timer = null',
' let frameIndex = 0',
'',
' function stopAnimation(ctx) {',
' if (timer) {',
' clearInterval(timer)',
' timer = null',
' }',
' frameIndex = 0',
' ctx.ui.setTitle(getBaseTitle(pi))',
' }',
'',
' function startAnimation(ctx) {',
' stopAnimation(ctx)',
' timer = setInterval(() => {',
' const frame = BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]',
' const cwd = process.cwd().split(/[\\\\/]/).filter(Boolean).at(-1) || process.cwd()',
' const session = pi.getSessionName()',
' const title = session ? `${frame} \\u03c0 - ${session} - ${cwd}` : `${frame} \\u03c0 - ${cwd}`',
' ctx.ui.setTitle(title)',
' frameIndex++',
' }, 80)',
' }',
'',
" pi.on('agent_start', async (_event, ctx) => {",
' startAnimation(ctx)',
' })',
'',
" pi.on('agent_end', async (_event, ctx) => {",
' stopAnimation(ctx)',
' })',
'',
" pi.on('session_shutdown', async (_event, ctx) => {",
' stopAnimation(ctx)',
' })',
'}',
''
].join('\n')
}
+53
View File
@@ -0,0 +1,53 @@
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, describe, expect, it } from 'vitest'
import { mirrorOmpPersistentSqliteFiles } from './omp-sqlite-overlay'
const tempDirs: string[] = []
function makeTempDir(): string {
const dir = mkdtempSync(join(tmpdir(), 'orca-omp-sqlite-overlay-'))
tempDirs.push(dir)
return dir
}
describe('OMP SQLite overlay persistence', () => {
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
it('replaces overlay-local SQLite files with source-backed DB files', () => {
const root = makeTempDir()
const sourceDir = join(root, 'source-agent')
const overlayDir = join(root, 'overlay-agent')
mkdirSync(overlayDir, { recursive: true })
writeFileSync(join(overlayDir, 'agent.db'), 'overlay main')
writeFileSync(join(overlayDir, 'agent.db-wal'), 'overlay wal')
const mirroredEntries = mirrorOmpPersistentSqliteFiles(sourceDir, overlayDir)
expect(mirroredEntries).toContain('agent.db')
expect(mirroredEntries).not.toContain('history.db')
expect(readFileSync(join(sourceDir, 'agent.db'), 'utf8')).toBe('')
expect(existsSync(join(sourceDir, 'agent.db-wal'))).toBe(process.platform === 'win32')
writeFileSync(join(overlayDir, 'agent.db'), 'new credentials')
expect(readFileSync(join(sourceDir, 'agent.db'), 'utf8')).toBe('new credentials')
if (process.platform !== 'win32') {
expect(lstatSync(join(overlayDir, 'agent.db')).isSymbolicLink()).toBe(true)
expect(existsSync(join(overlayDir, 'agent.db-wal'))).toBe(false)
}
})
})
+70
View File
@@ -0,0 +1,70 @@
// Why: OMP creates SQLite auth/history DBs lazily under PI_CODING_AGENT_DIR.
// If Orca only mirrors files that already exist, /login writes land in a
// disposable overlay instead of the user's ~/.omp/agent store.
import { closeSync, existsSync, mkdirSync, openSync } from 'fs'
import { join } from 'path'
import { mirrorWritableFileEntry, safeRemoveTree } from './overlay-mirror'
export const OMP_PERSISTENT_SQLITE_FILES = ['agent.db'] as const
const SQLITE_SIDECAR_SUFFIXES = ['-wal', '-shm'] as const
export function isOmpPersistentSqliteEntry(entryName: string): boolean {
return OMP_PERSISTENT_SQLITE_FILES.some(
(databaseName) =>
entryName === databaseName ||
entryName === `${databaseName}-wal` ||
entryName === `${databaseName}-shm`
)
}
function ensureEmptyFile(path: string): void {
closeSync(openSync(path, 'a'))
}
function mirrorOmpSqliteFile(
sourcePath: string,
overlayPath: string,
databaseName: string
): string[] {
if (!existsSync(sourcePath)) {
ensureEmptyFile(sourcePath)
}
safeRemoveTree(overlayPath)
for (const sidecarSuffix of SQLITE_SIDECAR_SUFFIXES) {
safeRemoveTree(`${overlayPath}${sidecarSuffix}`)
}
mirrorWritableFileEntry(sourcePath, overlayPath)
const mirroredEntries = [databaseName]
if (process.platform === 'win32') {
// Why: Windows hardlinks do not redirect SQLite's derived WAL filenames.
// Link sidecars too so lazy WAL writes still land in ~/.omp/agent.
for (const sidecarSuffix of SQLITE_SIDECAR_SUFFIXES) {
const sourceSidecar = `${sourcePath}${sidecarSuffix}`
const sidecarName = `${mirroredEntries[0]}${sidecarSuffix}`
ensureEmptyFile(sourceSidecar)
mirrorWritableFileEntry(sourceSidecar, `${overlayPath}${sidecarSuffix}`)
mirroredEntries.push(sidecarName)
}
}
return mirroredEntries
}
export function mirrorOmpPersistentSqliteFiles(
sourceAgentDir: string,
overlayDir: string
): string[] {
mkdirSync(sourceAgentDir, { recursive: true })
return OMP_PERSISTENT_SQLITE_FILES.flatMap((databaseName) =>
mirrorOmpSqliteFile(
join(sourceAgentDir, databaseName),
join(overlayDir, databaseName),
databaseName
)
)
}
+21
View File
@@ -39,6 +39,27 @@ export function mirrorEntry(sourcePath: string, targetPath: string): void {
symlinkSync(sourcePath, targetPath, isDirectoryLike ? 'dir' : 'file')
}
export function mirrorWritableFileEntry(sourcePath: string, targetPath: string): void {
if (process.platform === 'win32') {
try {
linkSync(sourcePath, targetPath)
return
} catch {
// Cross-device homes cannot hardlink; try a file symlink so writable
// SQLite state can still flow to source instead of a disposable copy.
}
try {
symlinkSync(sourcePath, targetPath, 'file')
return
} catch {
throw new Error(`Unable to create source-backed writable file mirror: ${targetPath}`)
}
}
symlinkSync(sourcePath, targetPath, 'file')
}
// Exported for tests. A "descend candidate" is an entry whose children we
// should recurse into when tearing down the overlay. Anything that is a
// symlink (including a Windows directory junction) must NOT be a candidate
+23
View File
@@ -169,6 +169,29 @@ describe('PluginOverlayManager', () => {
)
})
it('source-backs lazy OMP agent.db on the relay', () => {
manager.setSources({ piExtensionSource: '// pi extension' })
const sourceDir = join(homeDir, '.omp', 'agent')
const firstDir = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
expect(firstDir).not.toBeNull()
const sourcePath = join(sourceDir, 'agent.db')
const overlayPath = join(firstDir!, 'agent.db')
const content = 'agent.db relay credentials'
expect(existsSync(sourcePath)).toBe(true)
expect(existsSync(overlayPath)).toBe(true)
expect(existsSync(join(sourceDir, 'history.db'))).toBe(false)
writeFileSync(overlayPath, content)
expect(readFileSync(sourcePath, 'utf8')).toBe(content)
const secondDir = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
expect(secondDir).toBe(firstDir)
expect(readFileSync(join(secondDir!, 'agent.db'), 'utf8')).toBe('agent.db relay credentials')
})
// Why: per-agent overlay source dir. The renderer picks Pi or OMP per
// launch, and the relay must mirror the right `~/.<kind>/agent` source —
// disk-presence guessing (always-Pi or first-exists) shadows the other
+21 -6
View File
@@ -30,6 +30,10 @@ import {
import { homedir } from 'os'
import { basename, join } from 'path'
import { mirrorEntry, safeRemoveOverlay } from '../main/pty/overlay-mirror'
import {
isOmpPersistentSqliteEntry,
mirrorOmpPersistentSqliteFiles
} from '../main/pty/omp-sqlite-overlay'
import { mergePiOverlayUiSettings } from '../shared/pi-overlay-ui-settings'
import type { PiAgentKind } from '../shared/pi-agent-kind'
@@ -218,8 +222,11 @@ export class PluginOverlayManager {
return join(this.homeDir, PI_AGENT_HOME_DIR_NAME[kind], PI_AGENT_SUBDIR)
}
private mirrorPiAgentDir(sourceAgentDir: string, overlayDir: string): void {
private mirrorPiAgentDir(sourceAgentDir: string, overlayDir: string, kind: PiAgentKind): void {
if (!existsSync(sourceAgentDir)) {
if (kind === 'omp') {
mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
}
return
}
@@ -230,6 +237,10 @@ export class PluginOverlayManager {
continue
}
if (kind === 'omp' && isOmpPersistentSqliteEntry(entry.name)) {
continue
}
if (entry.name === 'extensions') {
const isSymlink = entry.isSymbolicLink()
let isLinkPointingToDir = false
@@ -260,6 +271,10 @@ export class PluginOverlayManager {
mirrorEntry(sourcePath, join(overlayDir, basename(sourcePath)))
}
if (kind === 'omp') {
mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
}
}
private readPiSettings(sourceAgentDir: string): unknown {
@@ -300,17 +315,17 @@ export class PluginOverlayManager {
const root = this.piRoots[kind]
const dir = join(root, safeDirName(id))
try {
const sourceAgentDir = existingAgentDir ?? this.getDefaultPiAgentDir(kind)
if (existingAgentDir && !existsSync(existingAgentDir)) {
return null
}
// Why: PI_CODING_AGENT_DIR is the whole state root for both Pi and OMP
// (OMP inherits the env-var name from Pi by design). Mirror the remote
// user's default agent dir so Orca's status extension does not hide auth,
// sessions, skills, prompts, themes, or user extensions inside SSH panes.
safeRemoveOverlay(dir, root)
mkdirSync(dir, { recursive: true })
const sourceAgentDir = existingAgentDir ?? this.getDefaultPiAgentDir(kind)
if (existingAgentDir && !existsSync(existingAgentDir)) {
return null
}
this.mirrorPiAgentDir(sourceAgentDir, dir)
this.mirrorPiAgentDir(sourceAgentDir, dir, kind)
this.writePiOverlaySettings(sourceAgentDir, dir)
const extensionsDir = join(dir, 'extensions')
mkdirSync(extensionsDir, { recursive: true })