Account for usage quota in the Forgejo scheduler #257
No reviewers
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck!257
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/issue-238-quota-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes: #238
The Forgejo dispatcher started turns regardless of how much of the model provider's subscription window was left; an exhausted quota only showed up as a generic failure comment after the turn had already failed.
What this adds
Quota reader (
apps/server/src/forgejo/quota.ts) resolves the provider behind thedefaultmodel role — the one a dispatcher-created session runs on — and scores the SDK'sUsageReports:scope.windowId === "extra"are dropped, so paid extra-usage credits never count. The exclusion is by id, not by an allowlist of5h/7d, so a future recurring window is gated automatically.overwhen that load reachesOMP_DECK_FORGEJO_QUOTA_MAX_PERCENT(new env entry, default 90, hot-applied — read fromprocess.envat every gate, so a Settings → Environment edit needs no restart).Gate in
ForgejoDispatcher.gate(), placed after every drop decision (so a row that would be dropped never posts a notice) and before an intake slot is taken (so a held row does not occupy one). A held row stayspendingand the thread gets one comment naming the measured percentage, the ceiling and — when every offending window reports one — the reset time. The comment id is persisted (migration 025) and the comment is deleted on every exit from the hold: the turn starting, the row being dropped, the row failing.Release. The hold is lifted by the next
wake()for any repository and by a shared timer armed for the reported reset, capped at 30 minutes. Re-reading usage is cheap: the SDK caches reports for 5 minutes. A row still over quota simply re-holds, and the notice is posted only once per hold, so a long hold does not grow a comment per re-check.Head-of-line. A hold can last hours, so actions
gate()completes without a turn —closed,reopened,implementation_abandoned,pushed,status— keep draining behind a held row. Without that, a close queued behind a held comment would not archive its session until the quota recovered.Fail-open where there is nothing to measure (no default model, a provider without a usage endpoint, no stored account); fail-closed where a measurement was expected but did not arrive, so a broken
/usagecannot hand the scheduler an unlimited budget.Not in scope, as planned:
startImplementer(the deck-UI start) stays ungated — it is user-initiated — and mid-turn exhaustion keeps today's generic failure comment.Verification
bun test src/forgejo/quota.test.ts— scoring: credits excluded, reset taken from the offending window and omitted when it has none, minimum across accounts,extra-only report unreadable, plus threshold parsing.bun test src/forgejo/dispatcher.test.ts— hold posts exactly one notice and leaves the row pending; the notice is not reposted on re-gating; flipping the quota dep tookruns the turn, deletes the notice and clears the stored id; an unreadable verdict holds too; aclosedrow queued behind a held row still archives its session.bun testinapps/server: 1304 pass, 1 fail — see below.bun run typecheck: one error, see below.End-to-end against a live deck (set the threshold to 1, mention the agent, watch the hold comment appear and disappear) was not run — this session has no deck instance or connected repository.
⚠️ One pre-existing failure, inherited from
mainmaindoes not typecheck and has one failing test, independent of this branch: the merge of #249 and #250 leftclearProgressLabelcalling the removedthis.swapLabels.Filed as #256 with the one-line fix. This PR deliberately does not include it to stay on topic; it should land separately. (This Forgejo repo runs no Actions workflow, so nothing reports it automatically — the gates are only red when run locally.)