fix(agent): cap figures + global region cap + reset active on stop
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m25s

Three safety/robustness fixes from the operator's run logs:

- Cap figures per frame (MAX_FIGURES, default 8) like components/panels already
  are. Uncapped, a huge/busy image yielded hundreds of figure boxes → hundreds
  of per-figure CCIP calls + crops → a 38s job AND a submit too big to accept
  (image 81602 looped on 413). This is the acute fix.
- Global per-JOB backstop (MAX_REGIONS, default 128): if total regions still
  exceed the cap (long video), keep the highest-scoring and log the drop, so a
  submit body can never blow past curator's limit.
- Stale "active" meter: stop() now resets _active to 0 (no slots remain, so the
  meter must read 0 at once), and _bump clamps at 0 so a slot finishing after the
  reset can't drive it negative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
2026-06-30 22:42:50 -04:00
parent 7e74fa767c
commit c587ac667c
4 changed files with 26 additions and 4 deletions
+16 -1
View File
@@ -210,6 +210,8 @@ class Worker:
self._ctrl_stop.set()
with self._lock:
slots, self._slots = self._slots, []
self._active = 0 # no slots left → the meter reads 0 at once; any
# lagging decrement is clamped (see _bump)
for s in slots:
s.stop.set() # each slot releases its inflight on exit
@@ -265,7 +267,9 @@ class Worker:
self.processed += processed
self.errors += errors
self.transient += transient
self._active += active
# Clamp at 0: a Stop resets _active to 0, so a slot that was mid-image
# decrements afterwards — that must not drive the meter negative.
self._active = max(0, self._active + active)
# --- per-slot loop -----------------------------------------------------
def _loop(self, slot: _Slot):
@@ -556,6 +560,17 @@ class Worker:
tmpl["embedding_version"] = embed_version
regions.append(tmpl)
self._record("gpu", time.monotonic() - _t_gpu)
# Backstop: never submit an unbounded pile of regions (a pathological
# image / long video). Keep the highest-scoring max_regions so the
# POST body stays sane — curator rejects an oversized one with 413
# (operator-flagged: image 81602 looped on 413).
if len(regions) > self.cfg.max_regions:
regions.sort(key=lambda r: r.get("score", 0.0) or 0.0, reverse=True)
dropped = len(regions) - self.cfg.max_regions
regions = regions[: self.cfg.max_regions]
log.info("job %s: capped regions %d%d (dropped %d)",
job.get("job_id"), len(regions) + dropped,
len(regions), dropped)
_t = time.monotonic()
self.client.submit(job["job_id"], regions, replace_kinds)
self._record("submit", time.monotonic() - _t)