mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
* fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823) Six shared files were rebuilt, erased or reported healthy after a read that had only failed. Batch A of the STA-4606 split: every one of these is reachable and testable on the host lane, so none of them wait on the WSL work. - `config-toml-trust.ts` upsertHookTrustEntries: `existsSync` reported a locked config.toml as absent, so the base content became '' and the upsert rewrote the file from the trust entries alone — a trust-only stub, with the user's model, provider, MCP servers, approvals and comments gone. It refuses now; every hook-service caller already turns that into "trust entries could not be written. Run /hooks in Codex to approve." - `codex-trust-grant-ledger.ts`: an unreadable ledger degraded to empty and the next write persisted a file holding only the home being written, dropping every other home's grants. The write paths refuse; the read path still degrades, and a corrupt ledger is still rebuilt. - `codex-pane-account-registry.ts`: an unreadable registry erased every pane's attribution AND cached that erasure, so it survived the file recovering. The failure is no longer cached, and both write sites refuse rather than persist a registry derived from an empty stand-in. - `hooks-json-read.ts`: the read arm already separated "no hooks" from "could not read", but the `existsSync` arm in front of it returned a valid empty config for a file that could not be opened. One read now classifies both. - `config-settings-baseline.ts`: absent, unparseable and unreadable all collapsed into `null`, so the snapshot rebuilt a baseline it could not read — recording an in-Codex edit as Orca's own write, after which promotion skips it forever. - `config-sync-stall.ts`: an unreadable runtime config read as absent and the status reported `synced` while the mirror was refusing. It reports `managed-home-unavailable`, the existing reason for exactly this, rather than borrowing a source-side one and blaming the wrong path. Absent and malformed still rebuild throughout — resetting corrupt state is the intent, and conflating it with unreadable would wedge a user on a broken file. * fix(codex): close shared state read-denial gaps * fix(codex): recover oversized settings baselines * fix(codex): name the stalled managed config * fix(codex): preserve hooks after failed source reads * test(codex): correct what the denyExistence rig actually models MEASURED on both platforms: a file-permission denial leaves existsSync TRUE and fails only the content read — chmod 000 gives EACCES on macOS, icacls /deny (R) gives EPERM errno -4048 on Windows, with stat/lstat succeeding in both. The rig's docblock claimed this mode modelled that denial. It does not. What it models is the UNC / \\wsl$ transport, where an unreachable distro reports errno UNKNOWN at every level and existsSync folds it to false. The distinction decides what the D29 guard is worth: under a permission denial the pre-fix code already failed safe, because existsSync was true so it took the read branch and threw. Only a transport that lies about existence reaches the rebuild-from-empty path. No behaviour change; the comment was wrong, not the code. * test(codex): exercise live baseline read denial * fix(codex): retry pane attribution writes * fix(codex): retry reconciliation registry writes * fix(codex): report unreadable sync baselines
131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
import { join } from 'node:path'
|
|
import { observeAgentStateFile } from './codex-path-observation'
|
|
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
|
|
import {
|
|
getCodexSettingsBaselinePath,
|
|
observeCodexSettingsBaseline
|
|
} from './config-settings-baseline'
|
|
import type { CodexSettingsPromotionHomes } from './config-settings-promotion'
|
|
import type {
|
|
CodexConfigSyncStallReason,
|
|
CodexConfigSyncStatus
|
|
} from '../../shared/codex-config-sync-types'
|
|
|
|
/**
|
|
* Reports whether the managed Codex runtime config is still tracking the user's
|
|
* real `~/.codex/config.toml`, and why it is not when it has fallen behind.
|
|
*
|
|
* The mirror preserves the managed runtime config when the source is missing or
|
|
* blank, which is silent by design — but the stall can persist for every launch
|
|
* (a downed WSL distro, an unhydrated cloud-synced home), leaving "Orca ignores
|
|
* my config edits" with nothing to diagnose. Derived on demand from the same
|
|
* predicates the mirror uses, so the two can never disagree.
|
|
*/
|
|
export function getCodexConfigSyncStatus(
|
|
homes: CodexSettingsPromotionHomes = {
|
|
runtimeHomePath: getOrcaManagedCodexHomePath(),
|
|
systemHomePath: getSystemCodexHomePath()
|
|
}
|
|
): CodexConfigSyncStatus {
|
|
const systemConfigPath = join(homes.systemHomePath, 'config.toml')
|
|
const runtimeConfigPath = join(homes.runtimeHomePath, 'config.toml')
|
|
// Why: a stall only withholds settings once a managed runtime config exists;
|
|
// without one the mirror seeds it and there is nothing yet to fall behind.
|
|
// But `existsSync` never proved the config readable, so this function could
|
|
// report `synced` while the mirror refused its content read — telling the
|
|
// user their edits had been applied when nothing had run.
|
|
const runtimeConfigObservation = observeAgentStateFile(runtimeConfigPath)
|
|
if (runtimeConfigObservation.kind === 'absent') {
|
|
return { state: 'synced', reason: null, systemConfigPath }
|
|
}
|
|
if (runtimeConfigObservation.kind === 'indeterminate') {
|
|
// Why: the managed home is what could not be read, so say that rather than
|
|
// borrowing a source-side reason and blaming the wrong path.
|
|
return {
|
|
state: 'stalled',
|
|
reason: 'managed-home-unavailable',
|
|
systemConfigPath,
|
|
managedStatePath: runtimeConfigPath
|
|
}
|
|
}
|
|
const baselineObservation = observeCodexSettingsBaseline(homes.runtimeHomePath)
|
|
if (baselineObservation.kind === 'indeterminate') {
|
|
return {
|
|
state: 'stalled',
|
|
reason: 'managed-home-unavailable',
|
|
systemConfigPath,
|
|
managedStatePath: getCodexSettingsBaselinePath(homes.runtimeHomePath)
|
|
}
|
|
}
|
|
const systemConfigObservation = observeAgentStateFile(systemConfigPath)
|
|
if (systemConfigObservation.kind === 'absent') {
|
|
return { state: 'stalled', reason: 'missing-source', systemConfigPath }
|
|
}
|
|
if (systemConfigObservation.kind === 'indeterminate') {
|
|
// Why: the mirror aborts on an unreadable source too, so report the stall
|
|
// rather than claiming a sync that cannot happen.
|
|
return { state: 'stalled', reason: 'unreadable-source', systemConfigPath }
|
|
}
|
|
const rawSystemConfig = systemConfigObservation.value
|
|
if (rawSystemConfig.trim() === '') {
|
|
return { state: 'stalled', reason: 'blank-source', systemConfigPath }
|
|
}
|
|
return { state: 'synced', reason: null, systemConfigPath }
|
|
}
|
|
|
|
// Why: the mirror runs on every launch and on the quota poll, so logging each
|
|
// skip would bury the log. Latch per home and log the transition instead, so an
|
|
// ongoing stall stays one line and a recovery is visible.
|
|
const stalledHomes = new Map<string, CodexConfigSyncStallReason>()
|
|
|
|
/**
|
|
* Logs a config-sync stall once per episode instead of once per sync pass, and
|
|
* logs again when the stall clears or its reason changes.
|
|
*
|
|
* Pass `mirrorError` from the mirror's catch path — an unreadable source throws
|
|
* out of the mirror, and without it that stall would log the raw failure on
|
|
* every launch and quota poll while never latching.
|
|
*/
|
|
export function reportCodexConfigSyncOutcome(
|
|
runtimeHomePath: string,
|
|
status: CodexConfigSyncStatus,
|
|
mirrorError?: unknown
|
|
): void {
|
|
const previousReason = stalledHomes.get(runtimeHomePath)
|
|
if (status.state === 'synced') {
|
|
if (previousReason) {
|
|
stalledHomes.delete(runtimeHomePath)
|
|
// Why: a removed runtime config also reads as synced, so this cannot
|
|
// claim the source became readable — only that the stall no longer holds.
|
|
console.warn(
|
|
`[codex-config] Config sync stall cleared for ${runtimeHomePath} (was ${previousReason}).`
|
|
)
|
|
}
|
|
if (mirrorError) {
|
|
// Why: the mirror still failed for some reason the stall check cannot
|
|
// name, so surface it rather than swallowing it behind a clean status.
|
|
console.warn('[codex-config] Failed to mirror system Codex config:', mirrorError)
|
|
}
|
|
return
|
|
}
|
|
if (previousReason === status.reason) {
|
|
return
|
|
}
|
|
stalledHomes.set(runtimeHomePath, status.reason)
|
|
if (status.reason === 'managed-home-unavailable') {
|
|
const managedStatePath = status.managedStatePath ?? join(runtimeHomePath, 'config.toml')
|
|
console.warn(
|
|
`[codex-config] Config sync stalled (${status.reason}): ${managedStatePath} is unusable, so ${runtimeHomePath} keeps its last synced settings. The managed state must be readable before settings can sync.`
|
|
)
|
|
return
|
|
}
|
|
console.warn(
|
|
`[codex-config] Config sync stalled (${status.reason}): ${status.systemConfigPath} is unusable, so ${runtimeHomePath} keeps its last synced settings. Edits to the source will not apply until it is readable.`
|
|
)
|
|
}
|
|
|
|
/** Clears the per-home episode latch; the latch is module state, so suites that assert on transitions need a clean slate between cases. */
|
|
export function resetCodexConfigSyncStallLatchForTests(): void {
|
|
stalledHomes.clear()
|
|
}
|