dsh-session-budget-guard
dsh-session-budget-guard
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 16, 2026
- Updated
- Sep 16, 2026
Introduction
dsh-session-budget-guard
English | 中文
A per-session-tree budget and burn-rate circuit breaker for DeepSeek Harness.
One prompt can fan out into a tree of subagents that all spend money at once. This plugin watches each tree — the root session plus every subagent descendant — and refuses the next model request (or the next delegation) when either the tree's total spend or its spend rate crosses a limit you set.
- insert:
- id: session-budget-guard
name: dsh-session-budget-guard
Why it exists
On 2026-09-16 a single session ("research a robot dog for the farm") fanned out three levels deep: 3 subagents, then 14, then 28 — 3 494 model calls in 20 minutes, 2 266 of them web searches. It cost ¥82 and drained the API balance; the Harness had no per-session budget, and a cumulative budget alone would have been too slow to matter, because the tree was spending roughly ¥4/minute.
That is why there are two independent gates: cumulative spend and spend rate.
Install
dsh plugin --profile web add dsh-session-budget-guard # npm (once published)
dsh plugin --profile web add github:erya2000/dsh-session-budget-guard
Or mount it by path from a local checkout (no install step):
# ~/.dsh/cordis.patch.yml
- insert:
- id: session-budget-guard
name: /absolute/path/to/dsh-session-budget-guard/lib/index.js
Restart the Harness once after installing (patch layers apply live, but the first mount of a new package needs a boot).
Configuration
Every key is optional; the defaults live in lib/rules.js. Unknown keys are
logged and ignored rather than silently dropped.
| Key | Default | Meaning |
|---|---|---|
mode | notify | notify = log + notify only. enforce = also refuse the next step, deny over-quota delegations/searches and stop the tree. |
enabled | true | Master switch. |
treeWarnCny | 20 | Warn once per tree at this cumulative spend. |
treeStopCny | 50 | Stop the tree at this cumulative spend. |
rateWarnPerMin | 4 | Warn when the tree grows faster than ¥4/min. |
rateStopPerMin | 8 | Stop the tree when it grows faster than ¥8/min. |
windowsSeconds | [60, 180] | Sliding windows used for the rate, in seconds. |
maxDelegationDepth | null (off) | Deepest allowed subagent depth. 1 means subagents may not spawn subagents. |
maxTreeNodes | null (off) | Most subagents allowed in one tree. |
maxSearchesPerSession | null (off) | Most web_search/web_fetch calls allowed per session. |
dailyWarnCny / dailyStopCny | 100 / 300 | Reported in warnings for the whole day across trees. |
stopStreak | 2 | Consecutive over-limit checks before acting. |
notifyCooldownSeconds | 300 | Minimum gap between notifications for the same tree. |
notify | auto | auto (macOS: osascript), none, or a shell command template containing {text}. |
exchangeRate | 7.2 | Used to convert the cost-meter ledger (USD) into CNY. |
statePath / logPath | $DSH_HOME/state/... | Where persistent state and the log live. |
A configuration tuned for enforcement (this is what the incident above led to):
config:
mode: enforce
treeWarnCny: 5
treeStopCny: 10
rateWarnPerMin: 1.5
rateStopPerMin: 3
maxDelegationDepth: 1
maxTreeNodes: 6
maxSearchesPerSession: 40
What it does when a limit is hit
agent/pre-stepreturns{kind:'reject'}— the next model request never happens, so a stopped tree stops costing money immediately.tools/pre-executereturns{kind:'deny', reason}for a newsubagent/subagent_fork/workflowcall past the structure limits, or for aweb_search/web_fetchpast the quota. The reason is model-visible, so the agent adapts instead of retrying blindly.- Descendants are interrupted deepest-first, then the session turn is cancelled (
keepInbox, so queued messages survive). - The event is written to the log, and a notification is sent.
In notify mode only step 4 happens.
Where the numbers come from
- If the
dsh-cost-meterplugin is mounted, the guard reads its ledger through thecostMeterservice (complete history, user-edited prices; the ledger is USD and is converted withexchangeRate). - Otherwise the guard prices calls itself from the
llm/streamwaterfall using DeepSeek list prices (peak/off-peak, Beijing time) — no dependency, but the totals start when the plugin loads.
Lineage (which session is whose subagent) is read from each session log's first record — id, parentSession, origin, delegationDepth — plus the live session store. Message content is never read.
Limits
- It is an in-process guard: it stops a tree that is running, it does not resurrect accounting after a Harness restart.
notifymode is the default. Turning onenforcemeans the plugin will cancel a session; tune the thresholds to your own traffic first (run innotifyfor a day and read the log).- Rate limits need at least one sample older than the window, so the first minute of a fresh guard cannot trip on rate — cumulative and structure limits still apply.
- Plain forks (
origin=fork) are deliberately excluded from tree aggregation.
Tests
node --test
18 tests cover the rule engine (lineage, tree aggregation including cycles, sliding-window rate, threshold decisions, waivers, structure gates) and the plugin wiring against a fake host (reject/deny behaviour, search quota, own llm/stream accounting).
License
MIT