fix(agent-hooks): keep startup reconciliation install-only

This commit is contained in:
Brennan Benson
2026-08-28 01:27:53 -07:00
parent c9b38d4aca
commit 0819373c49
3 changed files with 65 additions and 14 deletions
@@ -46,6 +46,7 @@ import {
installManagedAgentHooks,
removeManagedAgentHooksAsync,
resolveStartupManagedHookAction,
shouldInstallStartupManagedAgentHook,
shouldContinueManagedHookStartup
} from './managed-agent-hook-controls'
@@ -292,6 +293,41 @@ describe('startup managed hook reconciliation (STA-5679)', () => {
expect(resolveStartupManagedHookAction(null)).toBe('install')
})
it('only allows startup installs for globally enabled and agent-enabled hooks', () => {
expect(shouldInstallStartupManagedAgentHook({ agentStatusHooksEnabled: false }, 'codex')).toBe(
false
)
expect(
shouldInstallStartupManagedAgentHook(
{ agentStatusHooksEnabled: true, disabledTuiAgents: ['codex'] },
'codex'
)
).toBe(false)
expect(
shouldInstallStartupManagedAgentHook(
{ agentStatusHooksEnabled: true, disabledTuiAgents: ['claude'] },
'codex'
)
).toBe(true)
})
it('does not remove disabled agents during startup install reconciliation', async () => {
const settings = {
agentStatusHooksEnabled: true,
agentCmdOverrides: {},
disabledTuiAgents: ['claude' as const]
}
mocks.detect.mockResolvedValue({ codex: { state: 'found' } })
await installManagedAgentHooks(settings, {
shouldContinue: (agent) => shouldContinueManagedHookStartup(false, settings, agent)
})
expect(mocks.removeClaude).not.toHaveBeenCalled()
expect(mocks.installClaude).not.toHaveBeenCalled()
expect(mocks.installCodex).toHaveBeenCalledTimes(1)
})
it('still removes through the explicit Settings toggle', async () => {
// Anchors the assertion above: the removers really are wired, so 'skip' is a behavioral
// difference rather than a vacuous constant.
@@ -54,6 +54,16 @@ export function resolveStartupManagedHookAction(
return isAgentStatusHooksEnabled(settings) ? 'install' : 'skip'
}
export function shouldInstallStartupManagedAgentHook(
settings: ManagedHookSettings,
agent: AgentHookTarget
): boolean {
return (
resolveStartupManagedHookAction(settings) === 'install' &&
!normalizeDisabledTuiAgents(settings?.disabledTuiAgents).includes(agent)
)
}
export function shouldContinueManagedHookStartup(
isQuitting: boolean,
settings: ManagedHookSettings,
+19 -14
View File
@@ -89,10 +89,11 @@ import {
sweepRestoredSubagentsWithoutLiveAgent
} from './agent-hooks/restored-subagent-liveness-sweep'
import {
applyAgentStatusHooksEnabled,
installManagedAgentHooks,
isAgentStatusHooksEnabled,
removeManagedAgentHooksAsync,
resolveStartupManagedHookAction,
shouldInstallStartupManagedAgentHook,
shouldContinueManagedHookStartup
} from './agent-hooks/managed-agent-hook-controls'
import { initCohortClassifier } from './telemetry/cohort-classifier'
@@ -3089,25 +3090,29 @@ void app.whenReady().then(async () => {
// ordered before managed-hook reconciliation — an incapable host must re-arm
// and complete the legacy real-home sweep first — but awaiting it inline
// stalled app init behind that session, so chain instead of blocking.
const realHomeCodexHookState = codexRuntimeHome.isHostSystemDefaultRealHomeSelected()
? ensureRealHomeCodexHookState({
hooksEnabled: isAgentStatusHooksEnabled(store.getSettings()),
userDataPath: app.getPath('userData')
}).catch((error: unknown) => {
console.warn('[codex-real-home-hooks] startup ensure failed:', error)
})
: Promise.resolve()
const startupManagedHookSettings = store.getSettings()
const shouldReconcileStartupManagedHooks =
shouldInstallManagedHooks(is.dev) &&
resolveStartupManagedHookAction(startupManagedHookSettings) === 'install'
const realHomeCodexHookState =
shouldReconcileStartupManagedHooks &&
shouldInstallStartupManagedAgentHook(startupManagedHookSettings, 'codex') &&
codexRuntimeHome.isHostSystemDefaultRealHomeSelected()
? ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: app.getPath('userData')
}).catch((error: unknown) => {
console.warn('[codex-real-home-hooks] startup ensure failed:', error)
})
: Promise.resolve()
// Why skip rather than remove when the off switch is set: the hook files are user-global but this
// decision reads only THIS profile's settings, so removing here deletes the hooks every other Orca
// instance depends on (STA-5679). Skipping already keeps removed hooks from reappearing on launch.
if (
shouldInstallManagedHooks(is.dev) &&
resolveStartupManagedHookAction(store.getSettings()) === 'install'
) {
if (shouldReconcileStartupManagedHooks) {
const managedHookStore = store
void realHomeCodexHookState
.then(() =>
applyAgentStatusHooksEnabled(true, managedHookStore.getSettings(), {
installManagedAgentHooks(managedHookStore.getSettings(), {
shouldHydrateShellPath: app.isPackaged,
onInstallError: recordManagedHookInstallFailure,
shouldContinue: (agent) => {