Say when a forge concept broke, instead of returning an empty result (#17) - #88
Merged
Merged
Conversation
…17) `executeForgeCommand` returns null for a timeout, a non-zero exit and an unparseable body alike, and all 20 call sites read null as "no results". Nothing reached stderr: logDebug sits behind CODEV_DEBUG, which nobody sets. Measured live on 2026-08-21. `recently-merged` on gitea against ~/dev/entriq pages pulls?state=closed at 48.1s for page one against a 30s timeout, so it timed out every time. getOverview calls it on a 30s TTL and is hit by both the dashboard /api/overview poll and by every `afx status` while Tower runs. It had been failing on every one of those calls, invisibly, for as long as that repo had that many PRs. Nobody noticed, because empty looks like a valid answer. An empty panel that means "broken" is worse than an error, because it is believable. The warning goes to stderr, not behind a debug flag -- a failure nobody is told about IS the defect. Once per concept+kind per process, because these callers are poll loops and a warning every tick is its own kind of unreadable; the message says it will not repeat, so later silence is not read as recovery. A concept that was erroring and starts timing out warns again, because that is new information. CODEV_FORGE_QUIET=1 silences it. A successful command with an empty body stays silent. That distinction is the whole point: if empty warns too, the warning means nothing. Classification is now shared with executeForgeCommandDetailed so the three entry points cannot drift, and fixing it exposed that the sync variant could never have reported either failure. Measured on node 20: exec exit 3 -> { code: 3 } exec timeout -> { killed: true, signal: 'SIGTERM' } execSync exit 3 -> { status: 3, signal: null } execSync timeout -> { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' } Reading only `code` lost every sync exit status and every sync timeout. Detailed does not warn: its whole purpose is to hand the failure to a caller that will report it, and warning there would double-report. Not done: migrating the 20 callers to the detailed variant so the dashboard can render "could not load" instead of an empty panel. This is the issue's stated minimum, and it covers all 20 without touching any of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #17 (the issue's stated minimum, covering all 20 call sites without touching any of them).
executeForgeCommandreturnsnullfor a timeout, a non-zero exit and an unparseable body alike, and all 20 call sites readnullas "no results". Nothing reached stderr —logDebugsits behindCODEV_DEBUG, which nobody sets.Measured live on 2026-08-21:
recently-mergedon gitea against~/dev/entriqpagespulls?state=closedat 48.1s for page one against a 30s timeout, so it timed out every time.getOverviewcalls it on a 30s TTL and is hit by both the dashboard/api/overviewpoll and by everyafx statuswhile Tower runs. It had been failing on every one of those calls, invisibly, for as long as that repo had that many PRs.Nobody noticed, because empty looks like a valid answer. An empty panel that means "broken" is worse than an error, because it is believable.
What it looks like now
stderr, not a debug flag. A failure nobody is told about is the defect.
Once per concept+kind per process. These callers are poll loops; a warning every tick is its own kind of unreadable. The message says it will not repeat, so later silence is not read as recovery. A concept that was erroring and starts timing out warns again — that is new information.
A successful command with an empty body stays silent. That distinction is the whole point: if empty warns too, the warning means nothing and gets tuned out. A concept explicitly disabled in config stays silent too — not configured is not broken.
A second defect found on the way
Classification is now shared with
executeForgeCommandDetailedso the three entry points cannot drift — and doing that exposed that the sync variant could never have reported either failure. Measured on node 20:Reading only
codelost every sync exit status and every sync timeout.executeForgeCommandDetaileddeliberately does not warn: its whole purpose is to hand the failure to a caller that will report it, and warning there would double-report.Not done
Migrating the 20 callers to the detailed variant so the dashboard can render "could not load" instead of an empty panel — the issue's "better" option. That is a per-caller UI decision; this covers all of them today.
Verification
tsc --noEmitcleansleep,exit 3, stderr-then-exit) rather than mocking the error shape — the whole defect is about what the runtime actually reports🤖 Generated with Claude Code