Skip to content

Say when a forge concept broke, instead of returning an empty result (#17) - #88

Merged
pseudoseed merged 1 commit into
mainfrom
fix/17-forge-swallows-timeout
Aug 24, 2026
Merged

Say when a forge concept broke, instead of returning an empty result (#17)#88
pseudoseed merged 1 commit into
mainfrom
fix/17-forge-swallows-timeout

Conversation

@pseudoseed

Copy link
Copy Markdown
Owner

Addresses #17 (the issue's stated minimum, covering all 20 call sites without touching any of them).

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.

What it looks like now

[forge] 'recently-merged' timed out after 31s (limit 30s) — reporting NO RESULTS, which is not the same as none.
        Command failed: scripts/forge/gitea/recently-merged.sh
        Further 'recently-merged' timeout failures this process are silent. CODEV_FORGE_QUIET=1 to silence entirely.

[forge] 'recently-closed' (sync) exited 4 — reporting NO RESULTS, which is not the same as none.
        gh: not authenticated
        Further 'recently-closed' exit-4 failures this process are silent. CODEV_FORGE_QUIET=1 to silence entirely.

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 executeForgeCommandDetailed so the three entry points cannot drift — and doing that 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.

executeForgeCommandDetailed deliberately 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

  • 5901 passed, 0 failed (full suite, local)
  • tsc --noEmit clean
  • 25 new tests, driving real commands (sleep, exit 3, stderr-then-exit) rather than mocking the error shape — the whole defect is about what the runtime actually reports
  • Both warnings rendered from the built module against real failing commands, and the empty-result case confirmed silent

🤖 Generated with Claude Code

…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>
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.

1 participant