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/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
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
},
}