feat(concurrency): provider-wide lens budget, drop the model cap #27

Merged
steve merged 2 commits from feat/provider-wide-lens-budget into main 2026-07-18 16:48:26 +00:00
2 Commits
Author SHA1 Message Date
steveandClaude Opus 4.8 6a74b64c7a fix(concurrency): address gadfly review — fail-open pool + doc drift
Build & push image / build-and-push (pull_request) Successful in 4s
Gadfly's own review of #27 surfaced a real robustness cluster (5 models,
error-handling) plus stale comments I missed.

Robustness — the flock permit pool could hang forever:
- tryAcquire swallowed os.OpenFile errors and treated every flock error as
  "busy", so a broken/missing pool dir (or a filesystem without flock) would
  spin-poll indefinitely; the fanout context is uncancellable and the per-lens
  timeout only starts AFTER acquire returns. Can't trigger in the deploy
  (entrypoint mkdir -p's the dir) but fixed defensively.
- tryAcquire now returns a structural error, distinguished from a healthy-full
  pool (EWOULDBLOCK = busy → keep polling). acquire FAILS OPEN on a structural
  error: logs once and runs the lens unthrottled rather than hanging the review.
- acquire uses time.NewTimer + Stop() (no per-poll timer leak on cancellation).
- activeLensSem warns on stderr when GADFLY_LENS_SEM_DIR is set but the size is
  invalid (was a silent degrade to unthrottled).
- New test: a broken pool dir fails open promptly.

Doc drift (stale references to the removed model cap):
- main.go defaultLensConcurrency + runSpecialists doc, entrypoint.sh status
  pre-seed + lane-launch comments, and the pre-existing lens_concurrency_test.go
  header all updated to the provider-wide-budget wording.

Accepted (graded real, not changed): all-models-start-at-once startup burst
(intended tradeoff) and index-0 sweep bias (cosmetic). One false positive
(one model using the whole budget is the intended lone-model behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-18 12:47:36 -04:00
steveandClaude Opus 4.8 74831368ab feat(concurrency): provider-wide lens budget, drop the model cap
Build & push image / build-and-push (pull_request) Successful in 4s
Gadfly review (reusable) / review (pull_request) Successful in 14m47s
Adversarial Review (Gadfly) / review (pull_request) Successful in 14m47s
Concurrency was two multiplicative gates in two processes: entrypoint.sh
capped MODELS-at-once per provider (GADFLY_PROVIDER_CONCURRENCY) while each
model's binary separately capped its own lenses (GADFLY_LENS_CONCURRENCY).
A model therefore held its whole model-slot until its LAST lens finished,
stalling the next model even with idle lens capacity.

Collapse to one throttle: a provider-wide lens budget shared across all of
that provider's models. entrypoint now runs every model in a lane at once and
seeds a single cross-process permit pool per lane (a dir of N flock files,
sized by GADFLY_PROVIDER_LENS_CONCURRENCY -> GADFLY_LENS_CONCURRENCY). Each
lens pass (review+recheck) acquires a permit before it runs and releases it
after, so a model winding down immediately yields its freed permits to
another model's queued lenses. flock auto-releases on process death, so a
killed/crashed model can't leak budget.

- cmd/gadfly/lenssem.go: the flock permit pool (+ lenssem_test.go).
- main.go: runSpecialists holds a shared permit per lens; fanout sized to the
  budget so a lone model can use all of it. Falls back to the in-process limit
  when no pool is set (local runs, tests).
- entrypoint.sh: drop provider_cap/DEFAULT_CONC; run_lane runs all models and
  seeds the per-lane pool.
- GADFLY_PROVIDER_CONCURRENCY / GADFLY_CONCURRENCY are now ignored; the
  reusable workflow marks provider_concurrency deprecated and stops forwarding
  it. Docs (README, CLAUDE.md, examples) updated per the maintenance rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-18 12:22:11 -04:00