Support WSL dotfile-symlink write-back and lengthen redeem timeout

- Preserve symlinked Codex config on WSL by writing through the
  existing file instead of atomic-rename, since \\wsl$ symlink
  metadata isn't reliably detected and rename would clobber the link.
- Tighten new ~/.codex directory creation to 0700 (holds auth.json).
- Give explicit reset-credit redemption a 30s backend timeout instead
  of the 10s background-poll default, since it's user-triggered.
- Read sleeping-agent session state from the worktree's actual
  execution-host partition instead of always the local one, so the
  headless-wake check works correctly for SSH-hosted worktrees.
- Isolate serve-sim watcher tests from the real $TMPDIR/serve-sim
  state file to avoid leaking unrelated events.
This commit is contained in:
Jinjing
2026-07-09 21:50:20 -07:00
parent 260a7d02b3
commit 443018cd83
8 changed files with 79 additions and 30 deletions
@@ -321,6 +321,9 @@ describe('codex settings write-back promotion', () => {
syncSystemConfigIntoManagedCodexHome()
expect(statSync(systemConfigPath()).mode & 0o777).toBe(0o600)
// The created ~/.codex itself is owner-only — it will also hold
// auth.json once the user signs in.
expect(statSync(join(tmpHome, '.codex')).mode & 0o777).toBe(0o700)
}
)
@@ -346,7 +349,6 @@ describe('codex settings write-back promotion', () => {
() => {
mkdirSync(join(tmpHome, '.codex'), { recursive: true })
const realConfigPath = join(tmpHome, 'dotfiles', 'config.toml')
mkdirSync(join(tmpHome, 'dotfiles'), { recursive: true })
symlinkSync(realConfigPath, systemConfigPath())
syncSystemConfigIntoManagedCodexHome()
+19 -3
View File
@@ -10,6 +10,7 @@ import {
} from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { writeFileAtomically } from '../codex-accounts/fs-utils'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
import {
createTomlLineScanState,
@@ -221,10 +222,25 @@ function promoteCodexRuntimeSettingsToSystemUnsafe(homes: CodexSettingsPromotion
}
// Why: a genuinely fresh host has no ~/.codex yet; without the directory
// the atomic write ENOENTs and the following mirror wipes the setting.
mkdirSync(systemHomePath, { recursive: true })
// Owner-only: the directory holds auth.json and the full user config.
mkdirSync(systemHomePath, { recursive: true, mode: 0o700 })
const writeTarget = resolvePromotionWriteTarget(systemTomlPath)
const systemContent = existsSync(writeTarget.path) ? readFileSync(writeTarget.path, 'utf-8') : ''
writeFileAtomically(writeTarget.path, upsertTopLevelSettingsInContent(systemContent, updates), {
// Why: a dangling dotfile-manager symlink can point into a directory tree
// that has not been materialized yet; preserve the link and create its real
// parent so the atomic temp file can be written beside the target.
mkdirSync(dirname(writeTarget.path), { recursive: true, mode: 0o700 })
const targetExists = existsSync(writeTarget.path)
const systemContent = targetExists ? readFileSync(writeTarget.path, 'utf-8') : ''
const nextContent = upsertTopLevelSettingsInContent(systemContent, updates)
if (targetExists && parseWslUncPath(writeTarget.path)) {
// Why: symlink metadata through the \\wsl$ 9P provider is not reliable
// enough for realpath/lstat detection, and an atomic rename would replace
// a WSL-side dotfile symlink with a plain file. Writing through the
// existing file preserves the Linux-side inode (and its mode).
writeFileSync(writeTarget.path, nextContent, 'utf-8')
return
}
writeFileAtomically(writeTarget.path, nextContent, {
mode: writeTarget.mode
})
}
@@ -213,6 +213,7 @@ describe('Codex WSL runtime hook install', () => {
...process.env,
PATH: `${binDir}:${process.env.PATH ?? ''}`,
WSL_DISTRO_NAME: 'Ubuntu',
ORCA_AGENT_HOOK_ENDPOINT: '',
ORCA_AGENT_HOOK_PORT: '43210',
ORCA_AGENT_HOOK_TOKEN: 'token',
ORCA_PANE_KEY: 'pane-1',
@@ -6,6 +6,17 @@ import { ServeSimStateWatcher, type ServeSimStateDetectedEvent } from './serve-s
const TEST_UDID = '11111111-2222-3333-4444-555555555555'
let isolatedStateDirSeq = 0
// Why: the default state dir is the real $TMPDIR/serve-sim — a leftover state
// file from an actual simulator session would leak extra events into tests
// that only exercise PTY ingestion. Point at a dir that never exists.
function createIsolatedWatcher(): ServeSimStateWatcher {
isolatedStateDirSeq += 1
return new ServeSimStateWatcher({
stateDir: join(tmpdir(), `orca-serve-sim-test-none-${process.pid}-${isolatedStateDirSeq}`)
})
}
async function waitForEvent(
events: ServeSimStateDetectedEvent[],
predicate: (event: ServeSimStateDetectedEvent) => boolean
@@ -68,7 +79,7 @@ describe('ServeSimStateWatcher', () => {
})
it('suppresses Orca-managed sessions only while they are marked managed', async () => {
const watcher = new ServeSimStateWatcher()
const watcher = createIsolatedWatcher()
const events: ServeSimStateDetectedEvent[] = []
const payload = JSON.stringify({
device: TEST_UDID,
@@ -95,7 +106,7 @@ describe('ServeSimStateWatcher', () => {
})
it('prunes worktree-scoped dedupe keys on forget so a re-bound worktree re-emits', () => {
const watcher = new ServeSimStateWatcher()
const watcher = createIsolatedWatcher()
const events: ServeSimStateDetectedEvent[] = []
const payload = JSON.stringify({
device: TEST_UDID,
@@ -122,7 +133,7 @@ describe('ServeSimStateWatcher', () => {
})
it('dedupes one helper without hiding a later helper for the same simulator', () => {
const watcher = new ServeSimStateWatcher()
const watcher = createIsolatedWatcher()
const events: ServeSimStateDetectedEvent[] = []
const firstPayload = JSON.stringify({
device: TEST_UDID,
@@ -148,7 +148,8 @@ describe('Codex backend rate-limit requests', () => {
idempotencyKey: 'redeem-timeout'
})
await vi.advanceTimersByTimeAsync(0)
expect(timeout).toHaveBeenCalledWith(10_000)
// Why: redeem is user-triggered, so it gets the longer redeem deadline.
expect(timeout).toHaveBeenCalledWith(30_000)
expect(readFileMock).toHaveBeenCalledWith(join('/managed/deadline-home', 'auth.json'), 'utf8')
timeoutController.abort(deadlineError)
+9 -3
View File
@@ -34,6 +34,9 @@ const RPC_TIMEOUT_MS = 10_000
const WSL_RPC_TIMEOUT_MS = 25_000
const PTY_TIMEOUT_MS = 15_000
const BACKEND_TIMEOUT_MS = 10_000
// Why: redeeming a reset credit is an explicit user action, not a background
// poll — give it more room before failing so a slow backend can still finish.
const REDEEM_BACKEND_TIMEOUT_MS = 30_000
const MAX_DIAGNOSTIC_OUTPUT_LENGTH = 100_000
export type FetchCodexRateLimitsOptions = {
@@ -289,8 +292,11 @@ function mapBackendRateLimitResetCredits(
}
}
function createBackendRequestSignal(callerSignal?: AbortSignal): AbortSignal {
const timeoutSignal = AbortSignal.timeout(BACKEND_TIMEOUT_MS)
function createBackendRequestSignal(
callerSignal?: AbortSignal,
timeoutMs = BACKEND_TIMEOUT_MS
): AbortSignal {
const timeoutSignal = AbortSignal.timeout(timeoutMs)
return callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal
}
@@ -424,7 +430,7 @@ export async function consumeCodexRateLimitResetCredit(options: {
if (!options.idempotencyKey.trim()) {
throw new Error('Codex reset idempotency key is required')
}
const signal = createBackendRequestSignal()
const signal = createBackendRequestSignal(undefined, REDEEM_BACKEND_TIMEOUT_MS)
const auth = await getCodexBackendAuthHeaders(options, signal)
if (!auth) {
throw new Error('Codex not signed in')
+21 -17
View File
@@ -14958,27 +14958,28 @@ describe('OrcaRuntimeService', () => {
[TEST_WORKTREE_ID]: makeWorktreeMeta({ isUnread: false })
}
const resumeSleepingAgents = vi.fn()
const getWorkspaceSession = vi.fn(() => ({
...getDefaultWorkspaceSession(),
sleepingAgentSessionsByPaneKey: {
'tab-1:leaf-1': {
paneKey: 'tab-1:leaf-1',
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
agent: 'codex',
providerSession: { key: 'session_id', id: 'session-1' },
prompt: 'test',
state: 'done',
capturedAt: 1,
updatedAt: 1,
origin: 'worktree-sleep'
}
}
}))
const runtime = new OrcaRuntimeService({
...store,
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
getWorkspaceSession: () => ({
...getDefaultWorkspaceSession(),
sleepingAgentSessionsByPaneKey: {
'tab-1:leaf-1': {
paneKey: 'tab-1:leaf-1',
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
agent: 'codex',
providerSession: { key: 'session_id', id: 'session-1' },
prompt: 'test',
state: 'done',
capturedAt: 1,
updatedAt: 1,
origin: 'worktree-sleep'
}
}
})
getWorkspaceSession
} as never)
runtime.setNotifier({
worktreesChanged: vi.fn(),
@@ -15008,6 +15009,9 @@ describe('OrcaRuntimeService', () => {
expect(result.activated).toBe(true)
expect(result.sleepingAgentWake).toBe('unsupported-headless')
// Why: sleeping records are host-partitioned; the check must read the
// repo's execution host partition, not always the local one.
expect(getWorkspaceSession).toHaveBeenCalledWith('local')
expect(resumeSleepingAgents).not.toHaveBeenCalled()
})
+10 -2
View File
@@ -127,7 +127,11 @@ import type {
WorkspaceSessionState,
DirEntry
} from '../../shared/types'
import { parseExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import {
getRepoExecutionHostId,
parseExecutionHostId,
type ExecutionHostId
} from '../../shared/execution-host'
import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
import type { RuntimeClientEvent } from '../../shared/runtime-client-events'
import { toRuntimeActivateWorktreeEvent } from '../../shared/runtime-client-events'
@@ -12665,8 +12669,12 @@ export class OrcaRuntimeService {
this.notifier?.resumeSleepingAgents?.(worktree.id)
sleepingAgentWake = 'requested'
} else if (
// Why: sleeping records are partitioned by execution host; reading
// only the local partition would miss slept agents on SSH-host
// worktrees and skip the headless warning for them.
Object.values(
this.store?.getWorkspaceSession?.().sleepingAgentSessionsByPaneKey ?? {}
this.store?.getWorkspaceSession?.(getRepoExecutionHostId(repo))
.sleepingAgentSessionsByPaneKey ?? {}
).some((record) => record.worktreeId === worktree.id)
) {
// Why: headless is only degraded when this worktree actually has a