Repository navigation
feat(spend-caps): stop a DeepSeek wallet at the floor the operator sets - #16
Conversation
A spend cap was modelled as a share of a rolling subscription window, so the harnesses that actually spend money had no cap at all: the code said one was impossible against DeepSeek because "a cap is a fraction of a window, and there is no window here". The window was never the point. The meter was. A cap is now one of two kinds. A subscription is held on a window that reached its percentage, as before; a wallet is held on money that fell to a floor, in the currency the vendor reports it in. Both answer the same withheld block, hibernate the same way, and free parked work the same way when the operator loosens either. DeepSeek's balance stops being a display string stuffed into the plan tier and becomes money per currency, compared as decimals rather than floats. The wallet was already read on every preflight; nothing here asks the vendor anything new.
… page The block drew a meter and a limiter per subscription window, which is nothing a wallet has: DeepSeek reports money, not a share of anything, so its card stood empty with the balance smuggled into the plan badge. A wallet currency now gets a line of its own — what is in it, and a field for the money the lab has to leave behind. There is no meter over it and no slider, because a wallet has no full to be a fraction of. A keystroke that would leave something the lab cannot compare against a balance is refused in the field, so a saved floor is never a refusal the operator has to decode. The Muse card stops claiming the CLI reports no token counts. It does report them, in its own session log; what is true is that the lab does not read it yet, and that is what the card now says.
Giving the cap a discriminator made every cap already on disk unreadable, and an unreadable settings document leaves the shipped defaults standing. A lab told to stop Claude at 80% would have come back up spending the whole ceiling, with nothing on screen to say the instruction had been dropped — the exact failure this setting exists to prevent, caused by the change meant to widen it. A stored cap with no kind is read as the window cap it was; there was no other kind when it was written.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
A floor is typed a character at a time, and the field kept every step of it as the cap itself: `.5` stayed `.5` and `5.` stayed `5.`, neither of which is money the protocol takes. The save was then refused over a schema the operator never saw, on a field that looked no different from one that had saved. The half-typed forms are settled where the caps are handed to the lab — `.5` is nought point five, `5.` is five, a lone point is no floor at all — so the draft can go on holding what they typed and the point survives their reach for the cents.
…l-audit # Conflicts: # apps/daemon/src/harness-allowance/harness-allowance-readings.test.ts # apps/daemon/src/harness-allowance/harness-block.ts # apps/daemon/src/investigation-status/status-server.test.ts # apps/daemon/src/subscription-allowance/subscription-block.test.ts # apps/dashboard/src/allowance-panel/allowance-list.test.tsx # apps/dashboard/src/allowance-panel/allowance-list.tsx # apps/dashboard/src/allowance-panel/allowance-panel.i18n.ts # apps/dashboard/src/allowance-panel/allowance-section.test.tsx # apps/dashboard/src/allowance-panel/allowance-section.tsx # apps/dashboard/src/allowance-panel/spend-cap-draft.test.ts # apps/dashboard/src/allowance-panel/wallet-floor-field.const.ts # apps/dashboard/src/allowance-panel/wallet-floor-field.tsx # packages/harness/src/harness-allowance/claude-allowance.ts # packages/harness/src/subscription-allowance/deepseek-allowance.ts # packages/harness/src/subscription-allowance/muse-allowance.ts # packages/harness/src/subscription-allowance/subscription-allowance.types.ts # packages/protocol/src/harness-allowance/harness-allowance.schema.ts # packages/protocol/src/spend-caps/spend-cap.test.ts # packages/protocol/src/spend-caps/spend-cap.ts # packages/protocol/src/subscription-allowance/subscription-allowance.types.ts
|
Rebased onto main after #11 landed, which renamed the allowance layer underneath this branch. Two things worth a reviewer's eye, because they were decisions rather than conflict resolution: One money field, not two. #11 landed Muse says it in the panel, not the protocol. #11 had Muse report the sentence "Meta publishes no balance" in the money field. With money now a decimal per currency, prose has nowhere honest to sit, so a reading that succeeded with neither meter says so on the card instead — an empty card would read as a reading that failed, which is what that sentence existed to prevent. Also carried over from #11: the spent-wallet verdict still drives Verified on the merge: typecheck, 800+ tests and the build all pass locally. |
A spend cap was modelled as a share of a rolling subscription window, so the two harnesses that
actually spend money had no cap at all — and the code said one was impossible, because "a cap is a
fraction of a window, and there is no window here". The window was never the point; the meter was. A
subscription meters windows, a wallet meters money, and the operator's instruction is the same in
both cases: stop spending this harness here.
A cap is now one of two kinds.
window_percentis what shipped.wallet_flooris money the lab hasto leave in a wallet, in the currency the vendor reports it in. Both are read the same way, answer
the same withheld block before a run is prepared, hibernate the same way, and free parked work the
same way when the operator loosens either. DeepSeek's balance stops being a display string smuggled
into the plan tier and becomes money per currency, compared as decimals rather than floats — the
wallet was already read on every preflight, so nothing here asks a vendor anything new.
The third commit is a bug this change would otherwise have shipped: giving the cap a discriminator
made every cap already on disk unreadable, and an unreadable settings document leaves the shipped
defaults standing. A lab told to stop Claude at 80% would have come back up spending the whole
ceiling with nothing on screen to say so. A stored cap with no kind is now read as the window cap it
was.
The DeepSeek card stops promising what it cannot do
It claimed "There is no cap". There is one now, and the card says where to set it.
The subscriptions page
Both shots are the same lab, the same daemon and the same readings a minute apart; only the
dashboard build differs. An account that meters neither windows nor money no longer draws an empty
meter list under itself, which is what the stray rule under Muse Code is in the before shot.
machine these shots came from holds no DeepSeek key, so the card shows the reading that
actually happened —
No DeepSeek key at ~/.openlab/deepseek.json. The row and its floor fieldare covered by rendering tests instead. Hand the lab a key and I will add the shot.
Muse Code
The card claimed "the CLI reports no token counts, so the lab cannot show you what a run has cost".
That is false:
muse execwrites agoal_usage_attributionrecord per model call into its ownsession log, with input, output, cached and reasoning token counts, and Meta publishes the price of
each in the model catalog the CLI caches. What is true is that the lab does not read either yet, and
that is what the card now says. Reading them is the next change, not this one; the card is not
pictured here because the badge beside it names the account Meta bills.
What is verified
pnpm checkis green: lint, typecheck, 800+ tests, build. The settings contract was driven againsta real daemon on a disposable
OPENLAB_HOME— a legacy cap and a wallet floor round-trip throughPUT /api/settings, and/api/subscriptionsserves the balances. The balance-to-withheld path iscovered by tests rather than by a live wallet, for the same reason the row is not pictured.
Written by Claude Opus 5 through the Claude Code harness.
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Note
Add wallet floor spend caps to stop DeepSeek when balance drops to operator-set minimum
WALLET_FLOORspend cap kind alongside the existingWINDOW_PERCENTkind, with schema validation, duplicate-prevention, and Decimal-based comparison inspend-cap.ts.withheldBalancesto compare per-currency wallet balances against configured floors, andharnessBlocknow returns a WITHHELD block (without areturnsAt) when a wallet is at or under its floor.balance: string | nullfield onHarnessAllowanceReadingandHarnessAllowancewith abalances: WalletBalance[]array; DeepSeek now returns per-currency amounts instead of a formatted string.WalletFloorFieldReact component in the allowance panel for editing per-currency floors inline, with decimal-only validation and partial-input preservation.SpendCapsSchemabackfillskind: 'window_percent'for legacy cap objects that lack akindfield, preserving backward compatibility.balancefield is removed from allowance readings and allowances — any consumers outside this PR that readbalancewill break.Macroscope summarized e296e2b.