Thirteen of our scheduled wrappers could not report a failure, whatever the Python underneath returned. They hardcoded exit 0 or had no exit statement at all. The backup wrapper logged the real exit code and then never passed it on, so Task Scheduler’s LastTaskResult would read 0 even if the backup failed.

The wrapper that logged the truth and dropped it

The backup module’s main() returns 1 on a real dump or upload failure. The wrapper wrote that code into its log, then ended without an exit statement. The scheduler never saw it.

The gateway wrapper had the same gap. It recorded python returned $code, then stopped without exiting with it, so a daemon crash would be hidden from the scheduler and from the task health check. The listener wrapper never checked $LASTEXITCODE at all. A crash in any one channel’s sweep fell straight through to a default exit 0.

Why we read every return convention first

The audit covered every non-register wrapper except three that another session had uncommitted edits in. Each wrapper was checked against its module’s own return-code semantics before it was changed, because non-zero doesn’t mean the same thing everywhere.

Thirteen wrappers were broken. The fix commit’s message counted eleven, but its own file list and diff show thirteen, including the four CI alert runners. Six others were correct on purpose: their modules return 0 for success even with findings and non-zero only for a real failure, and those wrappers already documented that “findings, not failure” convention. We left them alone and added a matching comment to two more wrappers for the same reason, with no behavior change. Blindly converting every swallowed code into a propagated one would have traded silence for noise.

The fix

The fix was the same shape each time. Capture $LASTEXITCODE the moment the call returns, and exit with it explicitly. The listener runs several sweeps, and it now keeps any non-zero code from them as the wrapper’s exit code.

Keeping it fixed

We added a regression test that scans every wrapper for the swallow pattern, and proved it fires against a synthetic reproduction of the bug. A second test launches a real powershell.exe subprocess and checks that exit codes propagate, since the whole fix rests on that mechanism.

The backup wrapper wrote the real exit code to its log and then never returned it, so the scheduler never saw it.