antcolony
All repositories: gitoria
5.3 KB
// quota.hl — the Claude usage of this host's Claude account (mission 020; concept §3 "Claude quota per account:// reserve threshold, e.g. no new starts above 70 % of the 5 h window").//// HOW (tested 2026-09-24, Claude Code 2.1.280 on Loreana, .scratch/m020/usage-probe/): `claude -p /usage` is a LOCAL// command — no model call: result `total_cost_usd: 0`, `num_turns: 0`, no tokens, ~1–4 s. With// `--output-format json --verbose` the answer is a message list whose assistant message carries// usage_report.rate_limits.limits = [{ kind: 'session', percent: 5, resets_at: '2026-09-24T18:19:59.77+00:00' },// { kind: 'weekly_all', percent: 41, … }, { kind: 'weekly_scoped', … }]// and the text "Current session: 5% used · resets …" (the fallback when the structured part is missing).// The probe runs with `--permission-mode plan --permission-prompts none --strict-mcp-config --model haiku// --max-turns 1` so that, if `/usage` ever stopped being local, it would still be one cheap refused turn;// a probe that cost anything is reported (`cost`).import { readJson, isList, isObject, isString } from './util.hl'import { runClaude } from './claude.hl'static digit = (c) => { return c >= 48 && c <= 57 }// integer division for non-negative numbers (no Math)static idiv = (a, b) => { return (a - a % b) / b }// days since 1970-01-01 of a civil date (H. Hinnant's days_from_civil)static daysFromCivil = (y0, m, d) => {let y = m <= 2 ? y0 - 1 : y0let era = idiv(y, 400)let yoe = y - era * 400let mp = m > 2 ? m - 3 : m + 9let doy = idiv(153 * mp + 2, 5) + d - 1let doe = yoe * 365 + idiv(yoe, 4) - idiv(yoe, 100) + doyreturn era * 146097 + doe - 719468}// 'YYYY-MM-DDTHH:MM:SS[.frac][Z|±HH:MM]' → epoch ms, or nullstatic isoToMs = (s) => {if (s == null || s.length < 19) { return null }let y = toNumber(s.slice(0, 4))let mo = toNumber(s.slice(5, 7))let d = toNumber(s.slice(8, 10))let h = toNumber(s.slice(11, 13))let mi = toNumber(s.slice(14, 16))let se = toNumber(s.slice(17, 19))if (y == null || mo == null || d == null || h == null || mi == null || se == null) { return null }let ms = ((daysFromCivil(y, mo, d) * 24 + h) * 60 + mi) * 60000 + se * 1000let i = 19if (i < s.length && s.charCodeAt(i) == 46) {i = i + 1let frac = ''while (i < s.length && digit(s.charCodeAt(i))) {frac = frac + s.slice(i, i + 1)i = i + 1}if (frac.length >= 3) { ms = ms + toNumber(frac.slice(0, 3)) }}if (i < s.length) {let sign = s.slice(i, i + 1)if ((sign == '+' || sign == '-') && s.length >= i + 6) {let oh = toNumber(s.slice(i + 1, i + 3))let om = toNumber(s.slice(i + 4, i + 6))if (oh != null && om != null) {let off = (oh * 60 + om) * 60000ms = sign == '+' ? ms - off : ms + off}}}return ms}// the number in 'Current session: 5% used' → 5 or nullstatic percentAfter = (text, label) => {let i = text.indexOf(label)if (i < 0) { return null }let j = i + label.lengthlet k = jwhile (k < text.length && digit(text.charCodeAt(k))) { k = k + 1 }if (k == j) { return null }return toNumber(text.slice(j, k))}// the probe's output (parsed JSON) → { percent, resetsMs, resetsAt, weekly, cost, how } or { error }static parseUsage = (value) => {let limits = nulllet text = nulllet cost = nulllet msgs = isList(value) ? value : [value]for (m of msgs) {if (isObject(m)) {if (m.type == 'assistant' && m.usage_report != null && m.usage_report.rate_limits != null && isList(m.usage_report.rate_limits.limits)) { limits = m.usage_report.rate_limits.limits }if (m.type == 'result') {if (isString(m.result)) { text = m.result }cost = m.total_cost_usd}}}if (limits != null) {let out = { percent = null resetsMs = null resetsAt = null weekly = null weeklyResetsMs = null cost = cost how = 'usage_report' }for (l of limits) {if (isObject(l) && l.kind == 'session') {out.percent = l.percentout.resetsAt = l.resets_atout.resetsMs = isoToMs(l.resets_at)}if (isObject(l) && l.kind == 'weekly_all') {out.weekly = l.percentout.weeklyResetsMs = isoToMs(l.resets_at)}}if (out.percent != null) { return out }}if (text != null) {let p = percentAfter(text, 'Current session: ')if (p != null) { return { percent = p resetsMs = null resetsAt = null weekly = percentAfter(text, 'Current week (all models): ') cost = cost how = 'text' } }}return { error = 'no "session" limit in the /usage answer' }}// runs the probe (bin = the Claude binary, dir = where its files go); calls done(reading) with// { percent, resetsMs, weekly, cost, … } or { error }. → the processstatic probeUsage = (bin, dir, done) => {let outFile = dir + '/.quota-probe.json'let errFile = dir + '/.quota-probe.err'let args = ['-p' '/usage' '--output-format' 'json' '--verbose' '--model' 'haiku' '--max-turns' '1' '--permission-mode' 'plan' '--permission-prompts' 'none' '--strict-mcp-config']return runClaude({ label = 'quota' folder = dir inFile = '/dev/null' outFile = outFile errFile = errFile bin = bin args = args timeout = '60' }, (x) => {if (x.timedOut) {done({ error = 'the /usage probe timed out (60 s)' })return null}let rj = readJson(outFile)if (rj.error != null) {done({ error = 'the /usage probe gave no JSON (exit ' + x.ev.exit + '): ' + rj.error })return null}done(parseUsage(rj.value))return null})}
Branches
- mainmain branch
Latest commits
- 3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre
- a6af7883tracker: worker box sees calendar.worldapi.org (login to copy)mre
- c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
- 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
- 7f9660eeState of 2026-09-27, before the move to gitoriamre