[object Object]

← back to Desktop Dotbar

dotbar/jev: price-independent daily call-COUNT cap as the real paid backstop

1ab5eb81a7cc849a99fe5b3a2b0fecfa3d1ea65b · 2026-09-23 12:45:20 -0700 · Steve Abrams

The dollar cap sums the ledger at a PLACEHOLDER $0.001/call rate, so with the
real TypeSafe price unknown it may never trip under cache-gated volume —
effectively uncapped in real dollars. Add maxPaidCallsPerDay (default 800, env
DOTBAR_JEV_MAX_CALLS) that bounds worst-case spend to (calls x unknown price)
regardless of the rate. countPaidCallsToday() counts today's app+api ledger
rows, fail-closes to Infinity on any read error, composes with the dollar cap
(paid fires only if under BOTH), reserves in-loop so a burst within one refresh
can't exceed the cap, and surfaces paidCallsToday/maxPaidCallsPerDay/
countCapBlocked on /api/jev. start-bar.command stages the arm flags OFF
(DOTBAR_JEV_PAID=0, auto-snapshotted in de2776c). Negative tests prove the
guard goes red on injected at-cap + burst faults and countPaidCallsToday
fail-closes; 12/12 pass. No paid call fired; bar stays on the $0 builtin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1ab5eb81a7cc849a99fe5b3a2b0fecfa3d1ea65b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 12:45:20 2026 -0700

    dotbar/jev: price-independent daily call-COUNT cap as the real paid backstop
    
    The dollar cap sums the ledger at a PLACEHOLDER $0.001/call rate, so with the
    real TypeSafe price unknown it may never trip under cache-gated volume —
    effectively uncapped in real dollars. Add maxPaidCallsPerDay (default 800, env
    DOTBAR_JEV_MAX_CALLS) that bounds worst-case spend to (calls x unknown price)
    regardless of the rate. countPaidCallsToday() counts today's app+api ledger
    rows, fail-closes to Infinity on any read error, composes with the dollar cap
    (paid fires only if under BOTH), reserves in-loop so a burst within one refresh
    can't exceed the cap, and surfaces paidCallsToday/maxPaidCallsPerDay/
    countCapBlocked on /api/jev. start-bar.command stages the arm flags OFF
    (DOTBAR_JEV_PAID=0, auto-snapshotted in de2776c). Negative tests prove the
    guard goes red on injected at-cap + burst faults and countPaidCallsToday
    fail-closes; 12/12 pass. No paid call fired; bar stays on the $0 builtin.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 jev-dots.js           | 65 ++++++++++++++++++++++++++++++++++++++++++++-------
 test/jev-dots.test.js | 46 +++++++++++++++++++++++++++++++++++-
 2 files changed, 102 insertions(+), 9 deletions(-)

diff --git a/jev-dots.js b/jev-dots.js
index 0c67a36..2b4f2c8 100644
--- a/jev-dots.js
+++ b/jev-dots.js
@@ -35,6 +35,11 @@ const CONFIG = {
   paidEnabled: process.env.DOTBAR_JEV_PAID === '1',
   // Hard daily spend cap (USD). Steve may set DOTBAR_JEV_CAP_USD=2; default $5.
   capUsd: Number(process.env.DOTBAR_JEV_CAP_USD || '5'),
+  // Price-INDEPENDENT hard daily call-count cap — the real backstop when the true
+  // per-call price is unknown. Bounds worst-case spend to (maxPaidCalls x price)
+  // regardless of the rate. DOTBAR_JEV_MAX_CALLS overrides; default 800
+  // (800 x $0.001 placeholder = $0.80; even at 10x real price = $8).
+  maxPaidCalls: Number(process.env.DOTBAR_JEV_MAX_CALLS || '800'),
   provider: process.env.DOTBAR_JEV_PROVIDER || 'typesafe',
   timeoutMs: Number(process.env.DOTBAR_JEV_TIMEOUT_MS || '800'),
   ledgerPath: path.join(os.homedir(), '.claude', 'cost-ledger.jsonl'),
@@ -51,10 +56,13 @@ const stats = {
   builtinCalls: 0,    // classifier decided via the $0 local path
   paidCalls: 0,       // classifier decided via the paid TypeSafe API
   paidFellBack: 0,    // paid attempted but errored -> builtin
-  capBlocked: 0,      // paid armed but daily cap reached -> builtin
+  capBlocked: 0,      // paid armed but daily $ cap reached -> builtin
+  countCapBlocked: 0, // paid armed but daily call-COUNT cap reached -> builtin
   paidEnabled: CONFIG.paidEnabled,
   capUsd: CONFIG.capUsd,
+  maxPaidCallsPerDay: CONFIG.maxPaidCalls,
   lastSpendUsd: 0,
+  paidCallsToday: 0,  // today's paid-call count read from the ledger (last refresh)
   updated: 0,
 };
 
@@ -146,6 +154,32 @@ function spendTodayUsd(readLedger) {
   }
 }
 
+// ---- daily call-COUNT cap (price-independent, fail-closed) -------------------
+// Count today's desktop-dotbar PAID classification rows in the cost ledger — each
+// paid call logs exactly one typesafe_jev row (app+api+today). This is the real
+// backstop: it bounds worst-case paid volume regardless of the unknown per-call
+// price. On ANY read failure returns Infinity so the cap is treated as reached ->
+// paid path is skipped -> no spend. Mirrors spendTodayUsd's fail-closed discipline.
+function countPaidCallsToday(readLedger) {
+  try {
+    const raw = readLedger();
+    if (!raw) return 0;
+    const today = new Date().toISOString().slice(0, 10);
+    let n = 0;
+    for (const line of raw.split('\n')) {
+      if (!line.trim()) continue;
+      let e; try { e = JSON.parse(line); } catch { continue; }
+      if (e && e.app === CONFIG.costApp && e.api === CONFIG.costApiKey
+          && String(e.ts || '').slice(0, 10) === today) {
+        n += 1;
+      }
+    }
+    return n;
+  } catch {
+    return Infinity; // cannot confirm under cap -> never spend
+  }
+}
+
 function logPaidCall(logSpend) {
   // Log every paid call to the cost ledger via cost-tracker's log.js. Best-effort:
   // a logging failure must never crash a refresh (but is counted).
@@ -225,6 +259,7 @@ function typeSafeClassify(state, deps) {
 async function classifyDots(rows, opts = {}) {
   const paidEnabled = opts.paidEnabled !== undefined ? opts.paidEnabled : CONFIG.paidEnabled;
   const capUsd = opts.capUsd !== undefined ? opts.capUsd : CONFIG.capUsd;
+  const maxPaidCallsPerDay = opts.maxPaidCallsPerDay !== undefined ? opts.maxPaidCallsPerDay : CONFIG.maxPaidCalls;
   const readLedger = opts.readLedger || (() => { try { return fs.readFileSync(CONFIG.ledgerPath, 'utf8'); } catch { return ''; } });
   const paidTransport = opts.paidTransport || typeSafeClassify;
   const logSpend = opts.logSpend || defaultLogSpend;
@@ -242,26 +277,40 @@ async function classifyDots(rows, opts = {}) {
     misses.push({ tty: row.tty, state, hash });
   }
 
-  // Compute today's spend ONCE per refresh so a burst of misses can't each blow past the cap.
+  // Compute today's spend AND paid-call COUNT ONCE per refresh so a burst of misses
+  // can't each blow past either cap before the ledger flushes.
   let spend = 0;
-  if (paidEnabled) { spend = spendTodayUsd(readLedger); stats.lastSpendUsd = Number.isFinite(spend) ? spend : stats.lastSpendUsd; }
+  let paidCount = 0;
+  if (paidEnabled) {
+    spend = spendTodayUsd(readLedger);
+    stats.lastSpendUsd = Number.isFinite(spend) ? spend : stats.lastSpendUsd;
+    paidCount = countPaidCallsToday(readLedger);
+    stats.paidCallsToday = Number.isFinite(paidCount) ? paidCount : stats.paidCallsToday;
+  }
 
   for (const m of misses) {
     stats.classifications++;
     let result = null;
-    // Paid path is structurally UNREACHABLE unless armed AND provably under the cap.
-    if (paidEnabled && spend < capUsd) {
+    const underDollarCap = spend < capUsd;
+    const underCountCap = paidCount < maxPaidCallsPerDay;
+    // Paid path is structurally UNREACHABLE unless armed AND provably under BOTH caps.
+    // The two caps compose: whichever limit is hit first wins.
+    if (paidEnabled && underDollarCap && underCountCap) {
       try {
         result = await paidTransport(m.state, {});
         stats.paidCalls++;
         logPaidCall(logSpend);
-        spend += 0.001; // reserve the just-spent call so the in-loop cap holds before the ledger flushes
+        spend += 0.001;   // reserve this call's $ so the in-loop $ cap holds before the ledger flushes
+        paidCount += 1;   // reserve this call's COUNT so a burst within one refresh can't exceed the count cap
       } catch {
         stats.paidFellBack++;
         result = classifyBuiltin(m.state); stats.builtinCalls++;
       }
     } else {
-      if (paidEnabled) stats.capBlocked++; // armed but cap reached -> fall back, no spend
+      // armed but a cap reached -> fall back, no spend. Attribute the block to the
+      // binding cap ($ checked first, matching the original single-cap behavior).
+      if (paidEnabled && !underDollarCap) stats.capBlocked++;
+      else if (paidEnabled && !underCountCap) stats.countCapBlocked++;
       result = classifyBuiltin(m.state); stats.builtinCalls++;
     }
     cache.set(m.tty, { hash: m.hash, color: result.color, source: result.source, confidence: result.confidence });
@@ -279,4 +328,4 @@ async function classifyDots(rows, opts = {}) {
 function getStats() { return { ...stats, cacheSize: cache.size }; }
 function _resetForTest() { cache.clear(); for (const k of Object.keys(stats)) if (typeof stats[k] === 'number') stats[k] = 0; }
 
-module.exports = { classifyDots, classifyBuiltin, extractState, stateHash, spendTodayUsd, getStats, LABELS, _resetForTest };
+module.exports = { classifyDots, classifyBuiltin, extractState, stateHash, spendTodayUsd, countPaidCallsToday, getStats, LABELS, _resetForTest };
diff --git a/test/jev-dots.test.js b/test/jev-dots.test.js
index 5a26b62..10af344 100644
--- a/test/jev-dots.test.js
+++ b/test/jev-dots.test.js
@@ -7,6 +7,9 @@ const jev = require('../jev-dots');
 
 const row = (tty, color, label, variant) => ({ tty, color, label, variant: variant || '', live: true, parked: false });
 const todayLine = (usd) => JSON.stringify({ app: 'desktop-dotbar', cost_usd: usd, ts: new Date().toISOString() });
+// A real paid-call ledger row carries app+api+cost (see defaultLogSpend / cost-tracker log.js).
+const paidLine = () => JSON.stringify({ app: 'desktop-dotbar', api: 'typesafe_jev', cost_usd: 0.001, ts: new Date().toISOString() });
+const nPaidLines = (n) => Array.from({ length: n }, paidLine).join('\n');
 
 test('builtin classifies synthetic states to sane colors', () => {
   const cases = [
@@ -95,7 +98,48 @@ test('paid fallback: transport error -> builtin color, never blank, never throws
   assert.equal(map.get('a').source, 'builtin');
 });
 
-test('paid used when armed + under cap + transport ok, and logs the spend', async () => {
+test('COUNT-CAP fail-closed: count >= cap -> paid transport is NEVER called, falls back to builtin', async () => {
+  jev._resetForTest();
+  let paidAttempts = 0;
+  const spyTransport = async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; };
+  // capUsd high so the DOLLAR cap can't be the blocker; count cap of 3 with 3 rows already logged.
+  const map = await jev.classifyDots([row('a', 'purple', 'gated · 1'), row('b', 'green', 'working')], {
+    paidEnabled: true, capUsd: 100, maxPaidCallsPerDay: 3, readLedger: () => nPaidLines(3), paidTransport: spyTransport,
+  });
+  const s = jev.getStats();
+  assert.equal(paidAttempts, 0, 'no paid attempt once the call-count cap is reached');
+  assert.ok(s.countCapBlocked >= 2, 'count-cap block counted for each miss');
+  assert.equal(s.capBlocked, 0, 'the $ cap was NOT the binding limit here');
+  assert.equal(s.paidCalls, 0);
+  assert.equal(s.paidCallsToday, 3, 'reads the paid-call count from the ledger');
+  assert.ok(map.get('a').color && map.get('b').color, 'still shows colors (builtin fallback)');
+});
+
+test('COUNT-CAP boundary + burst: one call under cap fires, then the reserve blocks the next in the SAME refresh', async () => {
+  jev._resetForTest();
+  let paidAttempts = 0;
+  // cap=2, 1 paid row already today -> exactly ONE call of headroom. Two misses in one refresh:
+  // first fires (count 1<2), reserves to 2, second is blocked by the in-loop reservation.
+  const map = await jev.classifyDots([row('a', 'yellow', 'needs direction'), row('b', 'purple', 'gated · 1')], {
+    paidEnabled: true, capUsd: 100, maxPaidCallsPerDay: 2, readLedger: () => nPaidLines(1),
+    paidTransport: async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; },
+  });
+  const s = jev.getStats();
+  assert.equal(paidAttempts, 1, 'exactly one paid call fires before the reserve hits the cap');
+  assert.equal(s.paidCalls, 1);
+  assert.equal(s.countCapBlocked, 1, 'the second miss in the same refresh is count-cap blocked');
+  assert.ok(map.get('a') && map.get('b'));
+});
+
+test('countPaidCallsToday is fail-closed: a ledger-read throw returns Infinity (treated as at-cap)', () => {
+  assert.equal(jev.countPaidCallsToday(() => { throw new Error('ledger unreadable'); }), Infinity);
+  assert.equal(jev.countPaidCallsToday(() => ''), 0, 'empty ledger = 0 paid calls');
+  assert.equal(jev.countPaidCallsToday(() => nPaidLines(4)), 4, 'counts only app+api+today rows');
+  // A dollar-only synthetic row (no api) must NOT count as a paid call.
+  assert.equal(jev.countPaidCallsToday(() => todayLine(5.0)), 0, 'a $-only row without api=typesafe_jev is not a paid call');
+});
+
+test('paid used when armed + under BOTH caps + transport ok, and logs the spend', async () => {
   jev._resetForTest();
   let logged = 0;
   const map = await jev.classifyDots([row('a', 'yellow', 'needs direction')], {

← de2776c auto-data-snapshot: 2026-09-23T12:42:16 (1 data files) — sta  ·  back to Desktop Dotbar  ·  dotbar/jev: isolate tests from real cost ledger + reconcile a32c6bf →