mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user