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.
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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Stacked on #16 (retargets to develop when #16 merges).
Summary
Issue #4 item 2:
maxConcurrentAgentRunsandmaxModelCallsPerMinutewere enforced in memory, per JVM.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.maxRunSeconds.marketing_agent_run.heartbeat_atcolumn, written on start and before each model call. Hosts add the column (reference DDL updated).maxConcurrentAgentRunsoverride was never read; it now is.MarketingRuntime.Not done here (needs core): the count-then-start window is still racy across replicas. The
ConditionalUpdateActionclaim 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 verifygreen locally.Implementation by a Sonnet subagent, reviewed by Claude Opus.
🤖 Generated with Claude Code