mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
perf(fish): cache startup environment snapshot
This commit is contained in:
@@ -387,6 +387,75 @@ describe('readShellStartupEnvVar', () => {
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH)).toBe('/from/zzz')
|
||||
})
|
||||
|
||||
it('reads startup files once for agent homes without caching unrelated exports', () => {
|
||||
mockFishFiles(
|
||||
{
|
||||
'/home/alice/.config/fish/conf.d/10-agents.fish':
|
||||
'set -gx CODEX_HOME /from/confd\nset -gx UNQUERIED_SECRET old-secret\n',
|
||||
'/home/alice/.config/fish/config.fish':
|
||||
'set -gx CODEX_HOME /from/config\nset -gx OPENCODE_CONFIG_DIR /from/config-opencode\n'
|
||||
},
|
||||
['10-agents.fish']
|
||||
)
|
||||
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH)).toBe('/from/config')
|
||||
expect(readShellStartupEnvVar('OPENCODE_CONFIG_DIR', '/home/alice', FISH)).toBe(
|
||||
'/from/config-opencode'
|
||||
)
|
||||
expect(readdirSyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(readFileSyncMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
mockFishFiles(
|
||||
{
|
||||
'/home/alice/.config/fish/conf.d/10-agents.fish': 'set -gx UNQUERIED_SECRET new-secret\n',
|
||||
'/home/alice/.config/fish/config.fish': 'set -gx CODEX_HOME /changed\n'
|
||||
},
|
||||
['10-agents.fish']
|
||||
)
|
||||
expect(readShellStartupEnvVar('UNQUERIED_SECRET', '/home/alice', FISH)).toBe('new-secret')
|
||||
expect(readdirSyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(readFileSyncMock).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('isolates snapshots for distinct startup contexts', () => {
|
||||
readdirSyncMock.mockReturnValue([])
|
||||
mockStartupFiles({
|
||||
'/cfg-a/fish/config.fish':
|
||||
'set -gx CODEX_HOME /from/a\nset -gx OPENCODE_CONFIG_DIR /opencode/a\n',
|
||||
'/cfg-b/fish/config.fish':
|
||||
'set -gx CODEX_HOME /from/b\nset -gx OPENCODE_CONFIG_DIR /opencode/b\n'
|
||||
})
|
||||
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH, '/cfg-a')).toBe('/from/a')
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH, '/cfg-b')).toBe('/from/b')
|
||||
expect(readShellStartupEnvVar('OPENCODE_CONFIG_DIR', '/home/alice', FISH, '/cfg-a')).toBe(
|
||||
'/opencode/a'
|
||||
)
|
||||
expect(readShellStartupEnvVar('OPENCODE_CONFIG_DIR', '/home/alice', FISH, '/cfg-b')).toBe(
|
||||
'/opencode/b'
|
||||
)
|
||||
expect(readdirSyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(readFileSyncMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('evicts the oldest Fish startup context after the bounded cache fills', () => {
|
||||
readdirSyncMock.mockReturnValue([])
|
||||
existsSyncMock.mockReturnValue(true)
|
||||
readFileSyncMock.mockImplementation(
|
||||
(path: string) => `set -gx CODEX_HOME ${path.replace('/fish/config.fish', '')}\n`
|
||||
)
|
||||
|
||||
for (let index = 0; index < 33; index += 1) {
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH, `/cfg-${index}`)).toBe(
|
||||
`/cfg-${index}`
|
||||
)
|
||||
}
|
||||
expect(readFileSyncMock).toHaveBeenCalledTimes(33)
|
||||
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH, '/cfg-0')).toBe('/cfg-0')
|
||||
expect(readFileSyncMock).toHaveBeenCalledTimes(34)
|
||||
})
|
||||
|
||||
it('honors XDG_CONFIG_HOME', () => {
|
||||
mockFishFiles({ '/cfg/fish/config.fish': 'set -gx CODEX_HOME /from/xdg\n' })
|
||||
expect(readShellStartupEnvVar('CODEX_HOME', '/home/alice', FISH, '/cfg')).toBe('/from/xdg')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { posix } from 'node:path'
|
||||
import { BoundedMap } from '../../shared/bounded-map'
|
||||
|
||||
// Why: only files the user's actual shell would source. Mixing zsh and bash
|
||||
// files breaks the "last assignment wins matches the live shell" guarantee —
|
||||
@@ -16,6 +17,14 @@ const BASH_LOGIN_FILES = ['.bash_profile', '.bash_login', '.profile']
|
||||
const FISH_SNIPPET_DIR = 'conf.d'
|
||||
const FISH_SNIPPET_SUFFIX = '.fish'
|
||||
const FISH_CONFIG_FILE = 'config.fish'
|
||||
const FISH_AGENT_PATH_CONTEXT_LIMIT = 32
|
||||
const FISH_AGENT_PATH_NAMES = new Set([
|
||||
'CODEX_HOME',
|
||||
'GROK_HOME',
|
||||
'OPENCODE_CONFIG_DIR',
|
||||
'PI_CODING_AGENT_DIR',
|
||||
'PRIME_AGENT_CODING_AGENT_DIR'
|
||||
])
|
||||
|
||||
/** Assignment grammar of a startup file: `export NAME=value` vs fish `set -gx NAME value`. */
|
||||
type StartupFileSyntax = 'export' | 'fish-set'
|
||||
@@ -194,6 +203,37 @@ function expandHome(value: string, home: string): string {
|
||||
}
|
||||
|
||||
const cache = new Map<string, string | undefined>()
|
||||
const fishAgentPathCache = new BoundedMap<string, ReadonlyMap<string, string>>({
|
||||
maxEntries: FISH_AGENT_PATH_CONTEXT_LIMIT
|
||||
})
|
||||
|
||||
function readFishAgentPaths(
|
||||
home: string,
|
||||
shell: string,
|
||||
configHome: string | undefined
|
||||
): ReadonlyMap<string, string> {
|
||||
const cacheKey = `${home}\0${shell}\0${configHome ?? ''}`
|
||||
const cached = fishAgentPathCache.get(cacheKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const values = new Map<string, string>()
|
||||
for (const path of fishStartupFilePaths(home, configHome)) {
|
||||
const content = readStartupFile(path)
|
||||
if (content === null) {
|
||||
continue
|
||||
}
|
||||
for (const name of FISH_AGENT_PATH_NAMES) {
|
||||
const match = parseAssignedValue(content, name, home, 'fish-set')
|
||||
if (match !== undefined) {
|
||||
values.set(name, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
fishAgentPathCache.set(cacheKey, values)
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort static read of a single env-var assignment from the user's
|
||||
@@ -214,11 +254,13 @@ const cache = new Map<string, string | undefined>()
|
||||
* nothing. LAST matching assignment wins.
|
||||
* - fish universal variables (`set -Ux` stored in fish_variables) are only
|
||||
* seen when the assignment is also written in a config file.
|
||||
* - Fish shares one scan only for the agent-path variables Orca consumes;
|
||||
* arbitrary requested names retain the generic per-name behavior.
|
||||
* - Windows is unsupported (PowerShell profile parsing is out of scope).
|
||||
*
|
||||
* Results are memoized per (name, home, shell, configHome) for the process
|
||||
* lifetime — shell startup files do not change mid-session in any practical
|
||||
* scenario, and PTY spawn is on the hot path.
|
||||
* Results are memoized per variable for the process lifetime. Fish caches the
|
||||
* Orca-consumed set in bounded startup contexts, so those lookups avoid later
|
||||
* filesystem I/O without retaining unrelated exports or source text.
|
||||
*/
|
||||
export function readShellStartupEnvVar(
|
||||
name: string,
|
||||
@@ -235,20 +277,22 @@ export function readShellStartupEnvVar(
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (shell && posix.basename(shell).toLowerCase() === 'fish' && FISH_AGENT_PATH_NAMES.has(name)) {
|
||||
return readFishAgentPaths(home, shell, configHome).get(name)
|
||||
}
|
||||
|
||||
const cacheKey = `${name}\0${home}\0${shell ?? ''}\0${configHome ?? ''}`
|
||||
if (cache.has(cacheKey)) {
|
||||
return cache.get(cacheKey)
|
||||
}
|
||||
|
||||
let lastMatch: string | undefined
|
||||
|
||||
const { paths, syntax } = shellStartupFiles(home, shell, configHome)
|
||||
for (const path of paths) {
|
||||
const content = readStartupFile(path)
|
||||
if (content === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const match = parseAssignedValue(content, name, home, syntax)
|
||||
if (match !== undefined) {
|
||||
lastMatch = match
|
||||
@@ -294,4 +338,5 @@ export function readSessionShellStartupEnvVar(
|
||||
*/
|
||||
export function __resetShellStartupEnvCache(): void {
|
||||
cache.clear()
|
||||
fishAgentPathCache.clear()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user