Skip to content

release: v1.6.1 — essentials reach the worker, and APP_DEBUG means one thing - #126

Merged
hakeemRash merged 1 commit into
mainfrom
fix/worker-essentials-and-debug-flag
Aug 30, 2026
Merged

release: v1.6.1 — essentials reach the worker, and APP_DEBUG means one thing#126
hakeemRash merged 1 commit into
mainfrom
fix/worker-essentials-and-debug-flag

Conversation

@hakeemRash

Copy link
Copy Markdown
Contributor

Two bug fixes found while reading src/Kernel/ end to end. Merging this fires auto-release.yml: it reads ## [1.6.1] off the top of the CHANGELOG, tags v1.6.1, builds, publishes, and then the homebrew job bumps the formula's url + sha256 on its own.

Essential modules never reached a queued job

HttpPipeline passed its essentials into OnDemandLoader; WorkerLoop built its loader with none, and Kernel::materialize() had no way to hand them over. A module the project declared app-wide in proj.json "essentials" was app-wide for requests and absent from every job.

For an essential that rebinds a port per scope — tenancy rebinding DatabasePort — the failure is silent rather than loud: the binding still resolves, just to the wrong connection.

The worker now registers essentials into every job container and seeds their domains into the job's graph — the same two steps LoadStage performs for a request, so transitive requires[] come with them. A job whose class the manifest does not know now also gets a container rather than the bare CoreContainer, since "essential" means every unit of work. An application declaring no essentials keeps its previous behaviour, that fallback included.

The class→domain mapping both surfaces need moved to DependencyGraphCalculator::domainsFor(); a private copy in each pipeline is how they drifted apart in the first place.

APP_DEBUG meant two different things in one file

ErrorStage::isDebug() parsed the value with FILTER_VALIDATE_BOOL while publicError() compared it === 'true'. So APP_DEBUG=1 served the HTML debug page — stack trace and source excerpt — to anything sending Accept: text/html, while every JSON response still masked its message as "An internal error occurred.". One flag, two behaviours, and the more revealing of the two was the one that engaged.

There is now one isDebug(), used by both. FILTER_VALIDATE_BOOL is the surviving parse because it is what every other kernel flag uses (HttpPipeline::flag()), so 1, on, yes and true mean the same thing throughout. It also reads through env() rather than $_ENV/getenv(): the environment loader deliberately skips putenv(), so getenv() is not the source of truth for a .env value.

Note the direction — with APP_DEBUG=1 the JSON path now reveals exception messages, which is what the flag was asked for. Unset or falsy masks exactly as before.

Verification

  • 359 tests, 664 assertions (was 352; +7 new). PHPStan clean on all five changed files, no baseline additions.
  • Both new tests were run against the pre-fix code. ErrorStageDebugTest fails on APP_DEBUG=1 must reveal the message on the JSON path while its three masking assertions still pass — so the fix does not loosen masking. WorkerLoopEssentialsTest errors on Unknown named parameter $essentialModules, which proves it targets the new capability rather than a changed behaviour: the old loop had no way to be told about essentials at all.
  • Branched from origin/main rather than dev-mac, which still carries the unsquashed 1.6.0 commits already merged as release: v1.6.0 — make the queue an authenticated channel, and make BOOT_CACHE work #124.

Not touched: HomebrewFormula/hkm.rb. Its sha256 is written by the release job after publishing, and a digest cannot exist before the release does.

…e thing

Two surfaces disagreed with themselves.

WorkerLoop built its OnDemandLoader with no essentials, and the kernel had no
way to hand them over, so a module the project declared app-wide in proj.json
"essentials" was app-wide for requests and absent from every job. For an
essential that rebinds a port per scope — tenancy rebinding DatabasePort — the
failure is silent rather than loud: the binding still resolves, just to the
wrong connection. The worker now registers essentials into every job container
AND seeds their domains into the job's graph, the same two steps LoadStage
performs for a request, so their transitive requires[] come with them. A job
whose class the manifest does not know now also gets a container rather than
the bare CoreContainer, since "essential" means every unit of work; an
application declaring no essentials keeps its previous behaviour, that fallback
included. The class-to-domain mapping both surfaces need moved to
DependencyGraphCalculator::domainsFor() — a private copy in each pipeline is
how they drifted apart in the first place.

ErrorStage read APP_DEBUG two ways in one file: isDebug() parsed it with
FILTER_VALIDATE_BOOL while publicError() compared it === 'true'. So APP_DEBUG=1
served the HTML debug page — stack trace and source excerpt — to anything
sending Accept: text/html, while every JSON response still masked its message
as "An internal error occurred.". One flag, two behaviours, and the more
revealing of the two was the one that engaged. There is now one isDebug(), used
by both, keeping FILTER_VALIDATE_BOOL because that is what every other kernel
flag uses (HttpPipeline::flag()), and reading through env() rather than
$_ENV/getenv() — the environment loader deliberately skips putenv(), so getenv()
is not the source of truth for a .env value.

Both tests were checked against the pre-fix code: the debug test fails on
"APP_DEBUG=1 must reveal the message on the JSON path" while its three masking
assertions still pass, so the fix does not loosen masking.

359 tests, 664 assertions. PHPStan clean on every changed file, no baseline
additions.
@hakeemRash
hakeemRash merged commit 8b3252c into main Aug 30, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants