feat(agent): database-counted agent concurrency and model-call rate (#4, part 2) #18

Merged
KofTwentyTwo merged 1 commits from feature/distributed-limits into develop 2026-10-14 16:51:41 +00:00
KofTwentyTwo commented 2026-10-14 16:46:54 +00:00 (Migrated from github.com)

Stacked on #16 (retargets to develop when #16 merges).

Summary

Issue #4 item 2: maxConcurrentAgentRuns and maxModelCallsPerMinute were enforced in memory, per JVM.

  • Concurrency: preflight first fails stale RUNNING runs (no heartbeat for maxRunSeconds + 60 s: "Stopped responding; the server may have restarted."), then counts RUNNING runs across all products as the system user, and refuses with the existing plain reason at the limit.
  • Rate: counts model-call steps in the last 60 s across all runs. At the limit the run waits (re-reading Cancel), bounded by maxRunSeconds.
  • Heartbeat: new marketing_agent_run.heartbeat_at column, written on start and before each model call. Hosts add the column (reference DDL updated).
  • The policy's maxConcurrentAgentRuns override was never read; it now is.
  • In-memory semaphore and rate deque removed from MarketingRuntime.

Not done here (needs core): the count-then-start window is still racy across replicas. The ConditionalUpdateAction claim from qqq#1178 (QQQ 4.1.0-RC.7) closes it, with the execution claim (#4 item 1), after RC.7.

Tests

Two service instances sharing one backend cannot exceed the limit; a stale run is reaped while a fresh one still blocks; the rate limit delays rather than fails, and fails only when the run's time runs out. ./mvnw verify green locally.

Implementation by a Sonnet subagent, reviewed by Claude Opus.

🤖 Generated with Claude Code

Stacked on #16 (retargets to develop when #16 merges). ## Summary Issue #4 item 2: `maxConcurrentAgentRuns` and `maxModelCallsPerMinute` were enforced in memory, per JVM. - **Concurrency:** preflight first fails stale RUNNING runs (no heartbeat for `maxRunSeconds` + 60 s: "Stopped responding; the server may have restarted."), then counts RUNNING runs across all products as the system user, and refuses with the existing plain reason at the limit. - **Rate:** counts model-call steps in the last 60 s across all runs. At the limit the run **waits** (re-reading Cancel), bounded by `maxRunSeconds`. - **Heartbeat:** new `marketing_agent_run.heartbeat_at` column, written on start and before each model call. **Hosts add the column** (reference DDL updated). - The policy's `maxConcurrentAgentRuns` override was never read; it now is. - In-memory semaphore and rate deque removed from `MarketingRuntime`. **Not done here (needs core):** the count-then-start window is still racy across replicas. The `ConditionalUpdateAction` claim from qqq#1178 (QQQ 4.1.0-RC.7) closes it, with the execution claim (#4 item 1), after RC.7. ## Tests Two service instances sharing one backend cannot exceed the limit; a stale run is reaped while a fresh one still blocks; the rate limit delays rather than fails, and fails only when the run's time runs out. `./mvnw verify` green locally. Implementation by a Sonnet subagent, reviewed by Claude Opus. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.