From 3c7b3da17533f446a11409c2382eec5f7908e025 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 26 Jul 2026 19:48:06 +0000
Subject: [PATCH 1/2] fix(quota): give the per-run guard the boundary's
half-day cushion (#960 Edit)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The continuous boundary starts the week at zero, so the offset-less per-run
guard paused the user's own first run of the week the moment the agent
reported its first whole percent of usage — the stepped line it replaced had
always kept at least the current day's seventh in hand. The guard now carries
the same half-day cushion unattended work gets by default (a fixed one,
deliberately not the auto-spend slider: holding unattended work back must
never tighten the gate on work the user asked for), and the changeset no
longer claims the continuous boundary left the guard untouched.
Also rounds quotaHeadroom's refusal reason to one decimal (the half-day
default is 100/14, and fifteen digits of it said less), and corrects the
week fixtures' '5 days 19 hours' comments — the gap is 4 days 19 hours.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_019H7kazhsBn6Roz3xd5xdv3
---
.changeset/quota-boundary-continuous.md | 2 +-
packages/the-framework/src/auto-pm.test.ts | 16 +++++++++++++++-
packages/the-framework/src/auto-pm.ts | 5 ++++-
.../the-framework/src/consumption-guard.test.ts | 17 ++++++++++++++++-
packages/the-framework/src/consumption-guard.ts | 12 ++++++++++--
5 files changed, 46 insertions(+), 6 deletions(-)
diff --git a/.changeset/quota-boundary-continuous.md b/.changeset/quota-boundary-continuous.md
index 4f03e86e..dddc0cf2 100644
--- a/.changeset/quota-boundary-continuous.md
+++ b/.changeset/quota-boundary-continuous.md
@@ -2,4 +2,4 @@
'@gemstack/the-framework': patch
---
-The quota boundary (#879) now rises continuously with the clock — a plain pro-rated share of the elapsed week — instead of jumping to the next seventh once every 24 hours. A stepped boundary unlocked a whole day's worth of allowance the instant a new day began (including the entire week's worth on the last day), which read as generous on paper but let a burst of spending land the moment the clock ticked over rather than pacing with it. The usage panel's boundary line moves the same way it always did — it just draws whatever the framework reports — so this is purely a change to what auto PM and the panel treat as "on pace," not to the dashboard.
+The quota boundary (#879) now rises continuously with the clock — a plain pro-rated share of the elapsed week — instead of jumping to the next seventh once every 24 hours. A stepped boundary unlocked a whole day's worth of allowance the instant a new day began (including the entire week's worth on the last day), which read as generous on paper but let a burst of spending land the moment the clock ticked over rather than pacing with it. The usage panel's boundary line moves the same way it always did — it just draws whatever the framework reports. The per-run consumption guard (#519) measures against the same line, so it now carries the same half-day cushion unattended work gets by default: the continuous boundary starts the week at zero, and without the cushion the first whole percent the agent reports would have paused the user's own first run of the week — where the stepped line had always kept at least the current day's seventh in hand. The cushion is fixed, deliberately not the auto-spend slider: holding unattended work back must never tighten the gate on work the user asked for.
diff --git a/packages/the-framework/src/auto-pm.test.ts b/packages/the-framework/src/auto-pm.test.ts
index 1ea63a71..0d3da808 100644
--- a/packages/the-framework/src/auto-pm.test.ts
+++ b/packages/the-framework/src/auto-pm.test.ts
@@ -14,9 +14,10 @@ import {
type AutoPmProject,
} from './auto-pm.js'
import { quotaBoundaryStatus, type QuotaBoundaryStatus } from './quota-boundary.js'
+import { DEFAULT_SPEND_OFFSET } from './preference-defaults.js'
import { presets, type PresetKey } from './preset-catalog.js'
-/** 2026-07-20T12:00:00Z. The week below resets in 5 days 19 hours, so ~31.5% has elapsed (#960 Edit). */
+/** 2026-07-20T12:00:00Z. The week below resets in 4 days 19 hours, so ~31.5% has elapsed (#960 Edit). */
const T0 = Date.UTC(2026, 6, 20, 12, 0, 0)
/** A reading where the account's week is `weekPercent` used. */
@@ -88,6 +89,19 @@ test('quotaHeadroom stands down at the boundary, and says where it sits (#879)',
assert.match(decision.start === false ? decision.reason : '', /99% used, at or past day 3 of the week's 32%/)
})
+test('quotaHeadroom names a fractional offset to one decimal, not fifteen digits (#960 Edit)', () => {
+ // The half-day default is 100/14 — the reason line should say "+7.1", not the raw double.
+ const boundary = quotaBoundaryStatus({
+ windows: [{ label: 'Current week (all models)', kind: 'week', percentUsed: 99, resetsAtText: 'Jul 25 at 7am (UTC)' }],
+ now: T0,
+ limitOffset: DEFAULT_SPEND_OFFSET,
+ })
+ if (!boundary) throw new Error('the fixture week should be placeable')
+ const decision = quotaHeadroom(boundary)
+ assert.equal(decision.start, false)
+ assert.match(decision.start === false ? decision.reason : '', /your 39% limit \(\+7\.1 on the week's 32%\)/)
+})
+
test('quotaHeadroom stands down the moment the boundary is met, not only when it is passed (#879)', () => {
// Reads the boundary's own actual value back, rather than assuming a day/7 fraction (#960 Edit):
// percent is now the continuous elapsed share of the week, not a stepped one.
diff --git a/packages/the-framework/src/auto-pm.ts b/packages/the-framework/src/auto-pm.ts
index e5d35d15..fa266d6b 100644
--- a/packages/the-framework/src/auto-pm.ts
+++ b/packages/the-framework/src/auto-pm.ts
@@ -82,10 +82,13 @@ export function quotaHeadroom(quota: QuotaBoundaryStatus | undefined): QuotaDeci
if (reached) {
// Name the line it actually stopped at (#960). With the slider moved, saying "the week's 43%"
// when the run stopped at 63% would send someone looking for a bug that is a setting.
+ // The offset is rounded to one decimal for the sentence: a dragged slider stores integers,
+ // but the half-day default (#960 Edit) is 100/14 and fifteen digits of it would say less.
const { limit, boundary } = quota
+ const offsetText = `${limit.offset > 0 ? '+' : ''}${Math.round(limit.offset * 10) / 10}`
const line = limit.offset === 0
? `the week's ${Math.round(boundary.percent)}%`
- : `your ${Math.round(limit.percent)}% limit (${limit.offset > 0 ? '+' : ''}${limit.offset} on the week's ${Math.round(boundary.percent)}%)`
+ : `your ${Math.round(limit.percent)}% limit (${offsetText} on the week's ${Math.round(boundary.percent)}%)`
return {
start: false,
reason: `${reached.label} is ${Math.round(reached.percentUsed)}% used, at or past day ${boundary.day} of ${line}`,
diff --git a/packages/the-framework/src/consumption-guard.test.ts b/packages/the-framework/src/consumption-guard.test.ts
index c4a55fe0..7cf1ed90 100644
--- a/packages/the-framework/src/consumption-guard.test.ts
+++ b/packages/the-framework/src/consumption-guard.test.ts
@@ -4,7 +4,7 @@ import { startConsumptionGuard } from './consumption-guard.js'
import { FakeDriver } from './driver/index.js'
import type { Driver, DriverQuota, DriverQuotaWindow } from './driver/index.js'
-/** 2026-07-20T12:00:00Z. The week below resets in 5 days 19 hours, so ~31.5% has elapsed (#960 Edit). */
+/** 2026-07-20T12:00:00Z. The week below resets in 4 days 19 hours, so ~31.5% has elapsed (#960 Edit). */
const T0 = Date.UTC(2026, 6, 20, 12, 0, 0)
function quotaDriver(...readings: DriverQuota[]): Driver {
@@ -69,6 +69,21 @@ test("startConsumptionGuard brings the run's own model window into the gate (#87
onSonnet.stop()
})
+test('startConsumptionGuard keeps a half-day cushion, so a fresh week is not paused over its first percent (#960 Edit)', async () => {
+ // Half an hour into a fresh week the continuous boundary sits at ~0.3%, and the agent reports
+ // whole percentages — without the cushion, "1% used" would pause the user's own first run.
+ const halfHourIn = Date.UTC(2026, 6, 18, 7, 30, 0) // week runs Jul 18 7am -> Jul 25 7am (UTC)
+ const guard = startConsumptionGuard({ driver: quotaDriver(week(1), week(9)), now: () => halfHourIn })
+ assert.ok(guard)
+ // start() takes the first reading itself; let it land.
+ await new Promise(resolve => setImmediate(resolve))
+ assert.equal(guard.gate(), null)
+ // The cushion is half a day (~7.1 points), not a blank cheque: past it, the gate still bites.
+ await guard.poller.poll()
+ assert.equal(guard.gate(), 'Current week (all models)')
+ guard.stop()
+})
+
test('startConsumptionGuard gate carries on when the quota cannot be read (#531)', async () => {
const guard = startConsumptionGuard({ driver: quotaDriver({ available: false, reason: 'fetch-failed' }), now: () => T0 })
assert.ok(guard)
diff --git a/packages/the-framework/src/consumption-guard.ts b/packages/the-framework/src/consumption-guard.ts
index b86b810a..cfb31449 100644
--- a/packages/the-framework/src/consumption-guard.ts
+++ b/packages/the-framework/src/consumption-guard.ts
@@ -1,5 +1,6 @@
import { QuotaPoller } from './quota-poller.js'
import { quotaBoundaryStatus } from './quota-boundary.js'
+import { DEFAULT_SPEND_OFFSET } from './preference-defaults.js'
import type { Driver } from './driver/index.js'
/** A live quota gate, and the polling behind it. */
@@ -7,7 +8,8 @@ export interface ConsumptionGuard {
/**
* Pass as `consumptionGate` to a run. Answers from the poller's cached
* readings, so it is cheap enough to ask between every turn. The label of the
- * window that reached the boundary, or null while there is room.
+ * window that reached the limit in force (the boundary plus its half-day
+ * cushion, #960 Edit), or null while there is room.
*/
gate: () => string | null
/** The poller feeding it, exposed so a caller can read the windows off it. */
@@ -59,7 +61,13 @@ export function startConsumptionGuard(opts: StartConsumptionGuardOptions): Consu
gate: () => {
const windows = poller.current().lastGood?.windows
if (!windows) return null
- const status = quotaBoundaryStatus({ windows, now: now(), ...(opts.model ? { model: opts.model } : {}) })
+ // The same half-day cushion unattended work gets by default (#960 Edit). The continuous
+ // boundary starts the week at zero, so without one the first integer percent the agent
+ // reports outruns it and the user's own first run of the week is paused over ordinary
+ // rounding — the stepped line this replaces always kept the current day's seventh in hand.
+ // Deliberately not the user's slider: that sets where *unattended* work stands down, and
+ // holding it back must never tighten the gate on work the user asked for.
+ const status = quotaBoundaryStatus({ windows, now: now(), limitOffset: DEFAULT_SPEND_OFFSET, ...(opts.model ? { model: opts.model } : {}) })
return status?.reached?.label ?? null
},
}
From a560269ad5e694a96ef45ae3a9662dac8f126e2a Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 26 Jul 2026 19:48:13 +0000
Subject: [PATCH 2/2] fix(dashboard): show the spend-offset default in
Settings, retire stepped-boundary comments (#960 Edit)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Settings page's 'Spend offset' box still fell back to 0 when the
preference was unset, showing a value the daemon no longer uses now that the
real default is the half-day cushion — it reads the shared default (to one
decimal; saved values are integers, so the rounding only ever trims the
default). Also updates the comments that still explained thresholds by 'the
boundary steps a day at a time', which stopped being true when the boundary
went continuous, and RoutineWork's pointer at the usage panel's removed
autoPm toggle.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_019H7kazhsBn6Roz3xd5xdv3
---
packages/framework-dashboard/components/Quota.tsx | 6 +++---
packages/framework-dashboard/components/RoutineWork.tsx | 5 +++--
packages/framework-dashboard/components/SettingsPage.tsx | 9 ++++++---
packages/framework-dashboard/lib/quota-bar.test.ts | 4 ++--
packages/framework-dashboard/lib/quota-bar.ts | 5 +++--
5 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/packages/framework-dashboard/components/Quota.tsx b/packages/framework-dashboard/components/Quota.tsx
index 63646035..2f79ff0a 100644
--- a/packages/framework-dashboard/components/Quota.tsx
+++ b/packages/framework-dashboard/components/Quota.tsx
@@ -88,9 +88,9 @@ function WeekBar({
// How far ahead of the boundary's own pace the knob itself sits, as a duration — the same
// arithmetic as the main figure's deviation, but of the limit rather than actual consumption.
const limitDeviationMs = paceDeviationMs(limit, boundary.percent, boundary.resetsAt - boundary.startsAt)
- // A full day above the boundary, not merely past it — the boundary steps a day at a time on its
- // own, so a knob resting a few points ahead is normal and only worth flagging once it clears a
- // whole day's worth of pace.
+ // A full day above the boundary, not merely past it — the knob already rests half a day ahead
+ // by default, so a few points past the boundary is the normal state and only worth flagging
+ // once it clears a whole day's worth of pace.
const eagerConsumption = limit > boundary.percent + ONE_DAY_PERCENT
const label = `${Math.round(percentUsed)}% of the week used, against a boundary of ${Math.round(boundary.percent)}% on day ${boundary.day} of 7`
// How far ahead of or behind the boundary's own pace consumption is, as a duration (#960 Edit):
diff --git a/packages/framework-dashboard/components/RoutineWork.tsx b/packages/framework-dashboard/components/RoutineWork.tsx
index c127f2c1..2f6f681b 100644
--- a/packages/framework-dashboard/components/RoutineWork.tsx
+++ b/packages/framework-dashboard/components/RoutineWork.tsx
@@ -169,8 +169,9 @@ export function RoutineWork({
)}
- {/* The same `autoPm` preference the usage panel offers (#1161), which is the point: one
- switch, shown where the schedule it governs is listed. */}
+ {/* The same `autoPm` preference the Settings page offers (#1161), which is the point:
+ one switch, shown where the schedule it governs is listed. (The usage panel's copy
+ of it is gone, #960 Edit.) */}
diff --git a/packages/framework-dashboard/components/SettingsPage.tsx b/packages/framework-dashboard/components/SettingsPage.tsx
index 717e81b9..c110018a 100644
--- a/packages/framework-dashboard/components/SettingsPage.tsx
+++ b/packages/framework-dashboard/components/SettingsPage.tsx
@@ -1,6 +1,6 @@
import { useState, type ReactNode } from 'react'
import type { Preferences } from '@gemstack/the-framework'
-import { AGENTS, AGENT_LABELS, MAX_SPEND_OFFSET } from '@gemstack/the-framework/client'
+import { AGENTS, AGENT_LABELS, MAX_SPEND_OFFSET, DEFAULT_SPEND_OFFSET } from '@gemstack/the-framework/client'
import { useDetectedEditors } from '../lib/editors.js'
import { usePreferences, updatePreferences, themePreference, type ThemePreference } from '../lib/preferences.js'
import { runOptionRows, type OptionRow } from '../lib/run-option-rows.js'
@@ -201,11 +201,14 @@ export function SettingsPage({
onChange={next => updatePreferences({ autoPm: next })}
/>
{/* Bounded to the same ±MAX_SPEND_OFFSET the slider and the sanitizer use (#960). Without
- it a typed 9999 was clamped to 50 on save while the box kept showing 9999. */}
+ it a typed 9999 was clamped to 50 on save while the box kept showing 9999.
+ An untouched preference shows the real default in force — the half-day cushion
+ (#960 Edit), to one decimal — not a 0 the daemon isn't using. A saved value is an
+ integer, so the rounding only ever trims the default. */}
updatePreferences({ autoSpendOffset: value })}
diff --git a/packages/framework-dashboard/lib/quota-bar.test.ts b/packages/framework-dashboard/lib/quota-bar.test.ts
index cce4006c..ce8a743c 100644
--- a/packages/framework-dashboard/lib/quota-bar.test.ts
+++ b/packages/framework-dashboard/lib/quota-bar.test.ts
@@ -64,8 +64,8 @@ test('the built-in labels are a fixed two-letter notation, not the machine local
})
describe('quotaTone', () => {
- // The band exists so an account spending exactly as intended does not flip colour every day at
- // the moment the boundary steps a seventh.
+ // The band exists so an account spending exactly as intended does not flicker between colours
+ // for the ordinary jitter of readings either side of the line.
test('reads consumption against the boundary, with a band around it', () => {
expect(quotaTone(10, 43)).toBe('under')
expect(quotaTone(41, 43)).toBe('near')
diff --git a/packages/framework-dashboard/lib/quota-bar.ts b/packages/framework-dashboard/lib/quota-bar.ts
index 4e0bf593..18fe4598 100644
--- a/packages/framework-dashboard/lib/quota-bar.ts
+++ b/packages/framework-dashboard/lib/quota-bar.ts
@@ -96,8 +96,9 @@ export function quotaTone(percentUsed: number, boundaryPercent: number, band = N
/**
* A day, as a share of the week (#960 Edit): how far above the boundary the limit has to drift
- * before it's worth flagging as eager rather than merely past it. The boundary itself steps a
- * whole day at a time, so a knob resting a few points ahead of it is normal, not eager.
+ * before it's worth flagging as eager rather than merely past it. The limit already rests half a
+ * day ahead by default, so a knob a few points past the boundary is the normal state — only one
+ * that clears a whole day's worth of pace is asking for faster-than-the-week on purpose.
*/
export const ONE_DAY_PERCENT = 100 / 7