quota: z.ai credit-bucket back-pressure + resource gate + usage accounting (Redmine 490+491)
The loop now consults quota and host state before every dispatch and
DEFERS gated work with a logged reason instead of letting turns die at
the provider (the 2026-08-28 19:00 quota-wall failure mode, replayed as
a test). Adds internal/quota: 5h/weekly credit buckets (provider poll
when z.ai ships an endpoint - fake-server tested - else locally
estimated from the documented credit formula), TZ-aware peak window
(default 01:00-05:00 America/Chicago weekdays, matching the documented
z.ai peak Mon-Fri 14:00-18:00 Singapore), block/defer thresholds, a
read-only load/mem/disk/IO-PSI monitor, an optional redis shared-state
hop (stdlib RESP2 mini-client) so all instances of an account
coordinate, per-class token+credit accounting in loop.jsonl, and
`harness quota status|probe|gate`. Config: [quota] + [resources]
sections; README runbook covers the redis container and deploy-time
cgroup enforcement.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
+26
-1
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"ukrrs.com/mopac/harness/internal/models"
|
||||
"ukrrs.com/mopac/harness/internal/quota"
|
||||
"ukrrs.com/mopac/harness/internal/task"
|
||||
"ukrrs.com/mopac/harness/internal/writeback"
|
||||
)
|
||||
@@ -69,6 +70,9 @@ func (c *Conductor) RunLoop(ctx context.Context, opts LoopOpts) error {
|
||||
|
||||
fmt.Fprintf(c.out, "harness: loop: vertical=%s poll=%s state=%s redmine=%s (SIGINT to stop)\n",
|
||||
c.cfg.Vertical, interval, c.cfg.Loop.StateDir, c.redmineHost())
|
||||
if status := c.gateStatusLine(ctx); status != "off" {
|
||||
fmt.Fprintf(c.out, "harness: loop: gate: %s\n", status)
|
||||
}
|
||||
if gitea != nil {
|
||||
fmt.Fprintf(c.out, "harness: loop: gitea report commit on (%s/%s)\n", c.cfg.Gitea.Owner, c.cfg.Gitea.Repo)
|
||||
}
|
||||
@@ -121,6 +125,13 @@ func (c *Conductor) scanOnce(ctx context.Context, state *loopState, writer *writ
|
||||
fmt.Fprintf(c.out, "harness: loop: would dispatch %s (updated %s): %q\n", t.ID, t.UpdatedOn, t.Subject)
|
||||
continue
|
||||
}
|
||||
// Back-pressure: quota buckets + peak schedule + host resources. A
|
||||
// defer skips the turn WITHOUT consuming the task — the next scan
|
||||
// reconsiders it (the 19:00-wall class of failure becomes a logged
|
||||
// throttle instead of dead turns).
|
||||
if !c.gateConsult(ctx, state, t) {
|
||||
continue
|
||||
}
|
||||
c.dispatchTask(ctx, state, writer, gitea, t)
|
||||
}
|
||||
return nil
|
||||
@@ -136,9 +147,23 @@ func (c *Conductor) dispatchTask(ctx context.Context, state *loopState, writer *
|
||||
|
||||
run, err := c.runTask(ctx, t)
|
||||
if run != nil && run.turn != nil && run.reportPath != "" {
|
||||
// Usage accounting: tokens + estimated z.ai credits, per class,
|
||||
// into the JSONL (feeds the Discourse usage reports) and the
|
||||
// shared quota state (all instances' estimates).
|
||||
credits := 0.0
|
||||
if c.gate != nil {
|
||||
credits = c.gate.RecordTurn(quota.EstimateTurnInput{
|
||||
Model: run.decision.Model,
|
||||
PromptTokens: run.turn.PromptTokens,
|
||||
CompletionTokens: run.turn.CompletionTokens,
|
||||
Peak: c.gate.InPeak(),
|
||||
})
|
||||
}
|
||||
_ = state.log(loopEvent{
|
||||
Type: evReport, TaskID: t.ID, Model: run.decision.Model,
|
||||
Type: evReport, TaskID: t.ID, Model: run.decision.Model, Class: t.Class,
|
||||
ReportPath: run.reportPath, StopReason: run.turn.StopReason,
|
||||
PromptTokens: run.turn.PromptTokens, CompletionTokens: run.turn.CompletionTokens,
|
||||
TotalTokens: run.turn.TotalTokens, Credits: credits,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user