roundhouse finds support roots in config/application.rb by scanning for config.autoload_paths and config.eager_load_paths (src/ingest/app.rs:5613), and autoload_lib(ignore:) removes directories from them. It does not read config.autoload_once_paths. Rails loads those directories with its once autoloader, so their constants resolve in the app. roundhouse never reads the files and reports every reference to their constants as an error.
Repro
# config/application.rb
config.autoload_lib(ignore: %w[assets tasks autoload_once])
config.autoload_once_paths << "#{root}/lib/autoload_once"
# lib/autoload_once/gateway.rb
module Gateway
def self.ping = "pong"
end
# app/models/widget.rb
class Widget < ApplicationRecord
def ping = Gateway.ping
end
In Rails, Rails.autoloaders.once.dirs includes lib/autoload_once and Widget.new.ping returns "pong".
roundhouse check --continue . in the app root, main a3acfb4 (the absolute path of the temporary app is shortened to ./):
./app/models/widget.rb:2:14: error[unsupported]: constant not supported (all targets): Gateway
roundhouse-check: 2 gems: 1 framework, 1 infrastructure, 0 unknown
roundhouse-check: . — 0 parse error(s), 1 error(s), 0 warning(s), 0 gap-attributed note(s), 0 survey gap(s)
The release 2026.9.18 reports the same line as error[send_dispatch_failed]: no known method `ping` on Gateway.
Self-contained repro.rb (bundler/inline; exits 1 while the bug is present)
ruby repro.rb needs network access for bundler/inline and the roundhouse binary (on PATH, in $ROUNDHOUSE, or at /tmp/roundhouse-bin/roundhouse). It writes the app into a temporary directory, boots it in Rails and calls Widget#ping, generates Gemfile.lock with bundle lock --local, then runs roundhouse check --continue . on the same files.
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "~> 8.0"
gem "sqlite3"
end
require "fileutils"
require "tmpdir"
APP = {
"Gemfile" => <<~RUBY,
source "https://rubygems.org"
gem "rails", "~> 8.0"
gem "sqlite3"
RUBY
"config/application.rb" => <<~RUBY,
require "rails/all"
module Repro
class Application < Rails::Application
config.load_defaults 8.0
config.autoload_lib(ignore: %w[assets tasks autoload_once])
config.autoload_once_paths << "\#{root}/lib/autoload_once"
end
end
RUBY
"config/routes.rb" => <<~RUBY,
Rails.application.routes.draw do
end
RUBY
"db/schema.rb" => <<~RUBY,
ActiveRecord::Schema[8.0].define(version: 1) do
create_table "widgets", force: :cascade do |t|
t.string "name", null: false
t.timestamps
end
end
RUBY
"lib/autoload_once/gateway.rb" => <<~RUBY,
module Gateway
def self.ping = "pong"
end
RUBY
"app/models/application_record.rb" => <<~RUBY,
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
RUBY
"app/models/widget.rb" => <<~RUBY
class Widget < ApplicationRecord
def ping = Gateway.ping
end
RUBY
}
root = Dir.mktmpdir("roundhouse-repro")
at_exit { FileUtils.remove_entry(root) }
APP.each do |path, source|
FileUtils.mkdir_p(File.dirname(File.join(root, path)))
File.write(File.join(root, path), source)
end
puts "== Rails #{Rails.version} runs the app"
ENV["RAILS_ENV"] = "test"
ENV["DATABASE_URL"] = "sqlite3::memory:"
Dir.chdir(root) { require "./config/application" }
Rails.application.config.eager_load = false
Rails.application.initialize!
ActiveRecord::Schema.verbose = false
load File.join(root, "db/schema.rb")
once_dirs = Rails.autoloaders.once.dirs.filter_map { _1.dup.delete_prefix!("#{File.realpath(root)}/") }
puts "Rails.autoloaders.once.dirs under the app root -> #{once_dirs.inspect}"
puts "Widget.new.ping -> #{Widget.new.ping.inspect}"
raise "unexpected result" unless once_dirs.include?("lib/autoload_once") && Widget.new.ping == "pong"
puts "== roundhouse check --continue"
Bundler.with_unbundled_env { system("bundle", "lock", "--local", chdir: root, out: File::NULL, exception: true) }
roundhouse = ENV["ROUNDHOUSE"] ||
ENV["PATH"].split(File::PATH_SEPARATOR).map { File.join(_1, "roundhouse") }.find { File.executable?(_1) } ||
"/tmp/roundhouse-bin/roundhouse"
output = IO.popen([roundhouse, "check", "--continue", "."], chdir: root, err: [:child, :out], &:read)
puts output
exit 1 if output.include?("no known method `ping` on Gateway") ||
output.include?("constant not supported (all targets): Gateway")
Impact
In a Rails 8.0 app with a separate daemon, lib/autoload_once holds the client and protocol code that the app shares with the daemon. 179 of the app's 335 errors on main come from this: 178 constant not supported on the shared module and its nested constants, and one no known method `[]` on nil that follows from one of them.
Expected
autoload_once_paths entries are read like autoload_paths and eager_load_paths entries. Rails loads them once and never reloads them. This is the standard place for code that a non-Rails process shares with the app and that must not be reloaded. Removing a directory from autoload_lib and adding it to autoload_once_paths moves it to a different loader; it does not remove it from the app.
#86 and #94 (merged 2026-09-18) added the discovery of autoload_paths and eager_load_paths and the autoload_lib(ignore:) handling; autoload_once_paths was not part of that change. Since #503 (merged 2026-10-06) the unresolved constant is reported as error[unsupported] instead of a failed dispatch.
roundhouse 2026.9.18 (2e286e6), x86_64-unknown-linux-musl release binary, and main a3acfb4 built from source with cargo build --release --locked --bin roundhouse. Ruby 3.3.10, Rails 8.1.4, sqlite3 2.9.6, Linux x86_64, 2026-10-08.
Written with Claude Code (AI assistant) on behalf of @iberianpig.
roundhouse finds support roots in
config/application.rbby scanning forconfig.autoload_pathsandconfig.eager_load_paths(src/ingest/app.rs:5613), andautoload_lib(ignore:)removes directories from them. It does not readconfig.autoload_once_paths. Rails loads those directories with itsonceautoloader, so their constants resolve in the app. roundhouse never reads the files and reports every reference to their constants as an error.Repro
In Rails,
Rails.autoloaders.once.dirsincludeslib/autoload_onceandWidget.new.pingreturns"pong".roundhouse check --continue .in the app root, main a3acfb4 (the absolute path of the temporary app is shortened to./):The release 2026.9.18 reports the same line as
error[send_dispatch_failed]: no known method `ping` on Gateway.Self-contained repro.rb (bundler/inline; exits 1 while the bug is present)
ruby repro.rbneeds network access forbundler/inlineand the roundhouse binary (onPATH, in$ROUNDHOUSE, or at/tmp/roundhouse-bin/roundhouse). It writes the app into a temporary directory, boots it in Rails and callsWidget#ping, generatesGemfile.lockwithbundle lock --local, then runsroundhouse check --continue .on the same files.Impact
In a Rails 8.0 app with a separate daemon,
lib/autoload_onceholds the client and protocol code that the app shares with the daemon. 179 of the app's 335 errors on main come from this: 178constant not supportedon the shared module and its nested constants, and oneno known method `[]` on nilthat follows from one of them.Expected
autoload_once_pathsentries are read likeautoload_pathsandeager_load_pathsentries. Rails loads them once and never reloads them. This is the standard place for code that a non-Rails process shares with the app and that must not be reloaded. Removing a directory fromautoload_liband adding it toautoload_once_pathsmoves it to a different loader; it does not remove it from the app.#86 and #94 (merged 2026-09-18) added the discovery of
autoload_pathsandeager_load_pathsand theautoload_lib(ignore:)handling;autoload_once_pathswas not part of that change. Since #503 (merged 2026-10-06) the unresolved constant is reported aserror[unsupported]instead of a failed dispatch.roundhouse 2026.9.18 (2e286e6), x86_64-unknown-linux-musl release binary, and main a3acfb4 built from source with
cargo build --release --locked --bin roundhouse. Ruby 3.3.10, Rails 8.1.4, sqlite3 2.9.6, Linux x86_64, 2026-10-08.Written with Claude Code (AI assistant) on behalf of @iberianpig.