From 0819373c49f83e1a2239a1ea2be3b5f2e1ab927f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:27:53 -0700 Subject: [PATCH] fix(agent-hooks): keep startup reconciliation install-only --- .../managed-agent-hook-controls.test.ts | 36 +++++++++++++++++++ .../managed-agent-hook-controls.ts | 10 ++++++ src/main/index.ts | 33 +++++++++-------- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/main/agent-hooks/managed-agent-hook-controls.test.ts b/src/main/agent-hooks/managed-agent-hook-controls.test.ts index 204a004ec92..dd77b5e8d20 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.test.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.test.ts @@ -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. diff --git a/src/main/agent-hooks/managed-agent-hook-controls.ts b/src/main/agent-hooks/managed-agent-hook-controls.ts index 2a02acbee90..6c875257089 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.ts @@ -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, diff --git a/src/main/index.ts b/src/main/index.ts index 8b181a4d2fc..3ed31fadecc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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) => {