ruvnet/ruflo · error

actualUsd must be a non-negative finite number

Error message

actualUsd must be a non-negative finite number

What it means

BBS budget tracker commit() rejects its actualUsd with the same numeric contract as reserve(): finite and non-negative, validated before the transaction opens. The guard protects the commit path (ADR-164.1 §5.3) which charges the actual cost against the monthly budget and may transition an expired reservation to 'committed_post_expiry'.

Solutions

  1. Sanitize the metered amount before committing: const actual = Number.isFinite(x) && x >= 0 ? x : 0 (or drop the event and alert)
  2. Handle refunds/credits through a dedicated accounting path instead of negative commit() values
  3. Trace NaN sources in usage aggregation (undefined * number, parseFloat of malformed strings)
  4. Add a unit test asserting commit rejects NaN, Infinity, and -1 so regressions surface early

Example fix

// before
tracker.commit(reservationId, usage.actualUsd /* NaN when usage row missing */);
// after
const actual = Number(usage.actualUsd);
if (!Number.isFinite(actual) || actual < 0) throw new TypeError(`bad metered cost: ${usage.actualUsd}`);
tracker.commit(reservationId, actual);
Defensive patterns

Strategy: validation

Validate before calling

const actual = Number(meteredEvent.actualUsd);
if (!Number.isFinite(actual) || actual < 0) {
  deadLetter(meteredEvent); // never feed bad metering into commit()
} else {
  tracker.commit(reservationId, actual);
}

Type guard

function isCommitUsd(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v) && v >= 0;
}

Prevention

When it happens

Trigger: Calling tracker.commit(reservationId, actualUsd) with NaN/±Infinity/negative actualUsd — e.g. usage math that produced NaN, an API returning a negative refund/adjustment that was passed through, or a string amount from a metering event.

Common situations: Metering pipelines forwarding raw provider numbers without sanitization; negative adjustment/refund events that should go through a different path, not commit(); JSON deserialization yielding strings for numeric fields.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/ef947fa4728867f1. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/business-pods/bbs-budget-tracker.ts:299

      return { ok: true, reservationId, remainingAfterReserve: remaining };
    } catch (err) {
      if (transactionOpen) {
        try { rollbackStmt.run(); } catch { /* already-rolled */ }
      }
      throw err;
    }
  }

  /**
   * Commit the reservation with the actual cost. Late commits (expired
   * before commit landed) ARE accepted, transitioned to
   * 'committed_post_expiry', charged to the budget, and surfaced via
   * `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry`
   * audit emit. See ADR-164.1 §5.3 + §8.1.
   */
  commit(reservationId: string, actualUsd: number): CommitResult {
    if (!Number.isFinite(actualUsd) || actualUsd < 0) {
      throw new Error('actualUsd must be a non-negative finite number');
    }
    const nowMs = this.clock();

    const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
    const commitStmt = this.db.prepare('COMMIT');
    const rollbackStmt = this.db.prepare('ROLLBACK');

    beginStmt.run();
    let transactionOpen = true;
    try {
      const row = this.db
        .prepare(
          `SELECT state, room_id, estimated_usd, reserved_at, expires_at
             FROM bbs_budget_reservations
             WHERE reservation_id = ?`,
        )
        .get(reservationId) as
        | { state: string; room_id: string; estimated_usd: number; reserved_at: number; expires_at: number }

View on GitHub (pinned to fa13ee4ad6)