Is there an existing issue for this?
Is there a StackOverflow question about this issue?
What happened?
Component / versions
- androidx.appfunctions / appfunctions-compiler: 1.0.0-alpha09 (also reproduces on 1.0.0-alpha08)
- KSP: 2.3.8 (KSP2; KSP1 is no longer available in this release, so
ksp.useKSP2=false is not an escape hatch)
- Kotlin: 2.3.21
- AGP: 9.2.1, Gradle: 9.5.1
- compileSdk 37, targetSdk 36
- Multi-module project; all
@AppFunction / @AppFunctionSerializable sources and the aggregation live in the application module (:app), which also runs Hilt, Room, Glide, and Navigation SafeArgs through KSP. ksp { arg("appfunctions:aggregateAppFunctions", "true") } is set on :app only.
Summary
With incremental KSP enabled (the default), every run of the app module's KSP task fails in the AppFunctions aggregation round — including the first run from a fully pristine state (build/kspCaches and build/generated/ksp deleted):
e: [ksp] ksp.org.jetbrains.kotlin.analysis.api.lifetime.KaInvalidLifetimeOwnerAccessException:
Access to invalid ksp.org.jetbrains.kotlin.analysis.api.platform.lifetime.KotlinAlwaysAccessibleLifetimeToken@5a369373:
PSI has changed since creation
> Task :app:kspDefaultLocalDebugKotlin FAILED
Setting ksp.incremental=false in gradle.properties reliably avoids it, at the cost of full KSP reprocessing for every processor in the module (Hilt/Room/Glide included). The failure is deterministic in our module; we controlled for plausible confounders:
- Reproduces identically with a pristine KSP state (no
kspCaches, no prior generated outputs) — so it is not stale-state corruption from earlier builds; the invalid PSI handle arises within a single multi-round run.
- Reproduces identically after removing our own custom KSP processor from the module — not an interaction with third-party processors.
- We could not reproduce it in a minimal two-module sample (appfunctions + Hilt + a custom processor + 300 filler classes); the trigger appears to depend on module scale or generated-file/round count. Happy to provide further diagnostics from the failing module on request.
Steps to reproduce
Deterministic in our app module (single aggregating module; several thousand Kotlin sources; Hilt, Glide, and a custom processor running alongside the appfunctions compiler):
ksp.incremental unset (default true), ksp { arg("appfunctions:aggregateAppFunctions", "true") }.
./gradlew :app:kspDebugKotlin → fails with the exception above, on first runs and rebuilds alike.
- Set
ksp.incremental=false → always succeeds.
Analysis
The stack trace (attached, captured with --stacktrace) shows the crash fires inside the aggregation round's createNewFile(Dependencies.ALL_FILES, …) call — the line marked // TODO: Collect all AppFunction files as source files set at androidx-main commit a7958234146a3189a640b777ba9f113303d737db:
Caused by: ksp.org.jetbrains.kotlin.analysis.api.lifetime.KaInvalidLifetimeOwnerAccessException:
Access to invalid ...KotlinAlwaysAccessibleLifetimeToken@3d687a88: PSI has changed since creation
at ksp.org.jetbrains.kotlin.analysis.api.fir.symbols.KaFirFileSymbol.getPsi(KaFirFileSymbol.kt:65)
at com.google.devtools.ksp.impl.symbol.kotlin.KSFileImpl.getPsi(KSFileImpl.kt:46)
at com.google.devtools.ksp.impl.symbol.kotlin.KSFileImpl.getFilePath(KSFileImpl.kt:63)
at ksp.com.google.devtools.ksp.common.impl.CodeGeneratorImpl.associate(CodeGeneratorImpl.kt:200)
at ksp.com.google.devtools.ksp.common.impl.CodeGeneratorImpl.createNewFile(CodeGeneratorImpl.kt:171)
at androidx.appfunctions.compiler.processors.AppFunctionAggregateProcessor.generateAggregatedAppFunctionInventory(AppFunctionAggregateProcessor.kt:98)
at androidx.appfunctions.compiler.processors.AppFunctionAggregateProcessor.process(AppFunctionAggregateProcessor.kt:73)
Putting the pieces together:
AppFunctionAggregateProcessor deliberately defers to a late round — shouldProcess() returns true only once resolveAnnotatedAppFunctions() is empty, i.e. after earlier rounds have generated the per-class inventories/invokers/registry classes (AppFunctionAggregateProcessor.kt:191-195). By then, the rounds in between have generated many files (in our module: appfunctions per-class output plus Hilt and other processors).
- In that round,
createNewFile(Dependencies.ALL_FILES, …) makes KSP's CodeGeneratorImpl.associate walk every KSFile to record the new file's origins — this origin bookkeeping only happens in incremental mode, which is consistent with ksp.incremental=false avoiding the crash entirely (the walk never runs) rather than the staleness not existing. Some of the walked KSFileImpl handles refer to PSI invalidated by the generation that happened in the rounds since the handles were created, and the first getFilePath on a stale handle trips the Analysis API lifetime-token check.
Supporting data points:
Suggested directions
- Fix the
Dependencies.ALL_FILES TODOs in generateAggregatedAppFunctionInventory / generateAggregatedAppFunctionInvoker (this is the crashing call): pass the actual originating KSFiles of the discovered components (or Dependencies(aggregating = true) with the real sources) instead of ALL_FILES. Besides being the crash site, ALL_FILES also forces the aggregated outputs to regenerate on every incremental build.
- The discovery path has the same hazard one layer down:
AppFunctionSymbolResolver.filterAppFunctionComponent uses resolver.getDeclarationsFromPackage(...) (@KspExperimental) plus getClassDeclarationByName to re-resolve classes generated earlier in the same compilation (AppFunctionSymbolResolver.kt:352-356, :309-332). Discovering current-module components via getSymbolsWithAnnotation(@AppFunctionComponentRegistry) — they already carry that annotation — or passing qualified names in memory from the per-class processors (same provider instance) would avoid re-resolving freshly generated PSI; the package scan would then only be needed for classpath (AAR) components, which cannot change during compilation.
Workarounds
ksp.incremental=false in gradle.properties. Works reliably, but disables incremental KSP for every processor in the module (Hilt, Room, Glide included).
- Keeping all
@AppFunction sources in library modules so the aggregating app module runs shouldProcess()/aggregation in round 1 (verified above) — a significant restructuring for apps that, like ours, started with functions in the app module.
- Not workarounds:
ksp.useKSP2=false is rejected by KSP 2.3.8 ("KSP1 is no longer available"), and aggregateAppFunctions=false avoids the crash only by disabling inventory/invoker/index-XML generation entirely.
Full --stacktrace output attached.
Relevant logcat output
Code of Conduct
Is there an existing issue for this?
Is there a StackOverflow question about this issue?
What happened?
Component / versions
ksp.useKSP2=falseis not an escape hatch)@AppFunction/@AppFunctionSerializablesources and the aggregation live in the application module (:app), which also runs Hilt, Room, Glide, and Navigation SafeArgs through KSP.ksp { arg("appfunctions:aggregateAppFunctions", "true") }is set on:apponly.Summary
With incremental KSP enabled (the default), every run of the app module's KSP task fails in the AppFunctions aggregation round — including the first run from a fully pristine state (
build/kspCachesandbuild/generated/kspdeleted):Setting
ksp.incremental=falseingradle.propertiesreliably avoids it, at the cost of full KSP reprocessing for every processor in the module (Hilt/Room/Glide included). The failure is deterministic in our module; we controlled for plausible confounders:kspCaches, no prior generated outputs) — so it is not stale-state corruption from earlier builds; the invalid PSI handle arises within a single multi-round run.Steps to reproduce
Deterministic in our app module (single aggregating module; several thousand Kotlin sources; Hilt, Glide, and a custom processor running alongside the appfunctions compiler):
ksp.incrementalunset (default true),ksp { arg("appfunctions:aggregateAppFunctions", "true") }../gradlew :app:kspDebugKotlin→ fails with the exception above, on first runs and rebuilds alike.ksp.incremental=false→ always succeeds.Analysis
The stack trace (attached, captured with
--stacktrace) shows the crash fires inside the aggregation round'screateNewFile(Dependencies.ALL_FILES, …)call — the line marked// TODO: Collect all AppFunction files as source files setat androidx-main commita7958234146a3189a640b777ba9f113303d737db:Putting the pieces together:
AppFunctionAggregateProcessordeliberately defers to a late round —shouldProcess()returns true only onceresolveAnnotatedAppFunctions()is empty, i.e. after earlier rounds have generated the per-class inventories/invokers/registry classes (AppFunctionAggregateProcessor.kt:191-195). By then, the rounds in between have generated many files (in our module: appfunctions per-class output plus Hilt and other processors).createNewFile(Dependencies.ALL_FILES, …)makes KSP'sCodeGeneratorImpl.associatewalk everyKSFileto record the new file's origins — this origin bookkeeping only happens in incremental mode, which is consistent withksp.incremental=falseavoiding the crash entirely (the walk never runs) rather than the staleness not existing. Some of the walkedKSFileImplhandles refer to PSI invalidated by the generation that happened in the rounds since the handles were created, and the firstgetFilePathon a stale handle trips the Analysis API lifetime-token check.Supporting data points:
appfunctions:aggregateAppFunctions=false(which returns fromprocess()before any of this) never crashes — the per-class processors are fine incrementally.@AppFunctionsources while keepingaggregateAppFunctions=true.shouldProcess()then passes in round 1, before any generation has rebuilt the analysis session — and the build succeeds under incremental KSP, with$AggregatedAppFunctionInventory_Impl.ktgenerated (empty), on both a pristine-state run and an incremental rebuild. Restoring the@AppFunctionsources (aggregation back in a late round) restores the 100% crash. Same module, same processors, same incremental mode; the only variable is which round the aggregation'screateNewFile(ALL_FILES)executes in.PSI has changed since creationexception kpavlov/ksp-maven-plugin#22).Suggested directions
Dependencies.ALL_FILESTODOs ingenerateAggregatedAppFunctionInventory/generateAggregatedAppFunctionInvoker(this is the crashing call): pass the actual originatingKSFiles of the discovered components (orDependencies(aggregating = true)with the real sources) instead ofALL_FILES. Besides being the crash site,ALL_FILESalso forces the aggregated outputs to regenerate on every incremental build.AppFunctionSymbolResolver.filterAppFunctionComponentusesresolver.getDeclarationsFromPackage(...)(@KspExperimental) plusgetClassDeclarationByNameto re-resolve classes generated earlier in the same compilation (AppFunctionSymbolResolver.kt:352-356,:309-332). Discovering current-module components viagetSymbolsWithAnnotation(@AppFunctionComponentRegistry)— they already carry that annotation — or passing qualified names in memory from the per-class processors (same provider instance) would avoid re-resolving freshly generated PSI; the package scan would then only be needed for classpath (AAR) components, which cannot change during compilation.Workarounds
ksp.incremental=falseingradle.properties. Works reliably, but disables incremental KSP for every processor in the module (Hilt, Room, Glide included).@AppFunctionsources in library modules so the aggregating app module runsshouldProcess()/aggregation in round 1 (verified above) — a significant restructuring for apps that, like ours, started with functions in the app module.ksp.useKSP2=falseis rejected by KSP 2.3.8 ("KSP1 is no longer available"), andaggregateAppFunctions=falseavoids the crash only by disabling inventory/invoker/index-XML generation entirely.Full
--stacktraceoutput attached.Relevant logcat output
Code of Conduct