claude-flow 3.15.0 → 3.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.d.ts +139 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.js +358 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.js +65 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.d.ts +96 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.js +225 -0
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +74 -16
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +16 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +394 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.js +169 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.d.ts +55 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.js +329 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +6 -0
- package/v3/@claude-flow/cli/package.json +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.1",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BbsRoomBudgetTracker — Atomic reserve / commit / release token-bucket
|
|
3
|
+
* backed by better-sqlite3 (ADR-164.1 §3-§5).
|
|
4
|
+
*
|
|
5
|
+
* Closes the read-then-write race in ADR-164's first-draft tracker by routing
|
|
6
|
+
* every state transition through a single `BEGIN IMMEDIATE` transaction that
|
|
7
|
+
* acquires the SQLite write lock atomically with the gate check. The
|
|
8
|
+
* `_lock_bump` column write is a belt-and-suspenders explicit write inside
|
|
9
|
+
* the transaction so the lock acquisition is visible to code review and
|
|
10
|
+
* query-log analysis (§3.2 peer-review clarification).
|
|
11
|
+
*
|
|
12
|
+
* Architectural constraints (ADR-164.1):
|
|
13
|
+
* - Default OFF: this module is opt-in behind `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`.
|
|
14
|
+
* The pod-tick.mjs file-based stub remains the default until the flag is
|
|
15
|
+
* flipped per ADR-164.1 §12.4.
|
|
16
|
+
* - `committed_post_expiry` state machine implemented per §5.3 — late
|
|
17
|
+
* commits on expired reservations ARE accepted (the API spend already
|
|
18
|
+
* happened), transition to `committed_post_expiry`, charge the budget,
|
|
19
|
+
* and return `warned: 'COMMIT_AFTER_EXPIRY'`. This closes the §8.1
|
|
20
|
+
* Expired Commit Leak surfaced by peer review on 2026-06-29.
|
|
21
|
+
* - `expires_at` is clamped to [5_000, 300_000] ms (§3.2).
|
|
22
|
+
* - WAL + synchronous=NORMAL + busy_timeout=500 for write throughput.
|
|
23
|
+
*
|
|
24
|
+
* The sweeper interval (`sweepExpired()`) is exposed as a manual method;
|
|
25
|
+
* callers are expected to drive it on a setInterval — that integration lives
|
|
26
|
+
* at the call site, not in this file, per ADR-164.1 §7.1.
|
|
27
|
+
*
|
|
28
|
+
* @module @claude-flow/cli/business-pods/bbs-budget-tracker
|
|
29
|
+
*/
|
|
30
|
+
export interface SqliteDatabase {
|
|
31
|
+
pragma(name: string): unknown;
|
|
32
|
+
prepare(sql: string): SqliteStatement;
|
|
33
|
+
exec(sql: string): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
36
|
+
export interface SqliteStatement {
|
|
37
|
+
run(...params: unknown[]): {
|
|
38
|
+
changes: number;
|
|
39
|
+
lastInsertRowid: number | bigint;
|
|
40
|
+
};
|
|
41
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
42
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
43
|
+
}
|
|
44
|
+
export declare const RESERVATION_EXPIRY_FLOOR_MS = 5000;
|
|
45
|
+
export declare const RESERVATION_EXPIRY_CEILING_MS = 300000;
|
|
46
|
+
export declare const RESERVATION_EXPIRY_DEFAULT_MS = 60000;
|
|
47
|
+
export declare function clampReservationExpiry(raw: number | undefined): number;
|
|
48
|
+
export type ReserveResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
reservationId: string;
|
|
51
|
+
remainingAfterReserve: number;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
error: 'BUDGET_EXCEEDED' | 'ROOM_NOT_FOUND';
|
|
55
|
+
};
|
|
56
|
+
export type CommitResult = {
|
|
57
|
+
ok: true;
|
|
58
|
+
committed: true;
|
|
59
|
+
finalRemaining: number;
|
|
60
|
+
} | {
|
|
61
|
+
ok: true;
|
|
62
|
+
warned: 'COMMIT_AFTER_EXPIRY';
|
|
63
|
+
finalRemaining: number;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
error: 'NOT_FOUND' | 'ALREADY_FINALIZED';
|
|
67
|
+
};
|
|
68
|
+
export type ReleaseResult = {
|
|
69
|
+
ok: true;
|
|
70
|
+
released: true;
|
|
71
|
+
} | {
|
|
72
|
+
ok: false;
|
|
73
|
+
error: 'NOT_FOUND' | 'ALREADY_FINALIZED';
|
|
74
|
+
};
|
|
75
|
+
export declare const SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA busy_timeout = 500;\n\nCREATE TABLE IF NOT EXISTS bbs_budget_rooms (\n room_id TEXT NOT NULL PRIMARY KEY,\n monthly_cap_usd REAL NOT NULL CHECK (monthly_cap_usd >= 0),\n billing_month TEXT NOT NULL,\n _lock_bump INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS bbs_budget_reservations (\n reservation_id TEXT NOT NULL PRIMARY KEY,\n room_id TEXT NOT NULL REFERENCES bbs_budget_rooms(room_id),\n caller_node_id TEXT NOT NULL,\n estimated_usd REAL NOT NULL CHECK (estimated_usd >= 0),\n actual_usd REAL,\n state TEXT NOT NULL\n CHECK (state IN ('reserved','committed','released','expired','committed_post_expiry')),\n reserved_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n committed_at INTEGER,\n audit_envelope_id TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_room_month\n ON bbs_budget_reservations (room_id, reserved_at);\n\nCREATE INDEX IF NOT EXISTS idx_reservations_expiry\n ON bbs_budget_reservations (state, expires_at);\n";
|
|
76
|
+
export interface BudgetAuditSink {
|
|
77
|
+
emit(eventType: 'reservation.committed_post_expiry' | 'reservation.committed' | 'reservation.released' | 'reservation.reserved' | 'reservation.budget_exceeded', payload: Record<string, unknown>): void;
|
|
78
|
+
}
|
|
79
|
+
export interface AtomicBbsRoomBudgetTrackerOptions {
|
|
80
|
+
/** SQLite database handle (caller-owned, opened in WAL mode). */
|
|
81
|
+
db: SqliteDatabase;
|
|
82
|
+
/** Override the default 60s reservation window (clamped to [5s, 300s]). */
|
|
83
|
+
defaultExpiryMs?: number;
|
|
84
|
+
/** Audit sink — receives reservation lifecycle events. */
|
|
85
|
+
audit?: BudgetAuditSink;
|
|
86
|
+
/** Inject Date.now() for tests. */
|
|
87
|
+
clock?: () => number;
|
|
88
|
+
}
|
|
89
|
+
export declare class AtomicBbsRoomBudgetTracker {
|
|
90
|
+
private readonly db;
|
|
91
|
+
private readonly defaultExpiryMs;
|
|
92
|
+
private readonly audit;
|
|
93
|
+
private readonly clock;
|
|
94
|
+
constructor(opts: AtomicBbsRoomBudgetTrackerOptions);
|
|
95
|
+
/**
|
|
96
|
+
* Register (or update) a room and its monthly cap. Idempotent.
|
|
97
|
+
*/
|
|
98
|
+
registerRoom(roomId: string, monthlyCapUsd: number): void;
|
|
99
|
+
/**
|
|
100
|
+
* Atomically check budget and insert a reservation row in a single
|
|
101
|
+
* BEGIN IMMEDIATE transaction. See ADR-164.1 §5.2.
|
|
102
|
+
*/
|
|
103
|
+
reserve(roomId: string, callerId: string, estimatedUsd: number, opts?: {
|
|
104
|
+
auditEnvelopeId?: string;
|
|
105
|
+
expiryMs?: number;
|
|
106
|
+
}): ReserveResult;
|
|
107
|
+
/**
|
|
108
|
+
* Commit the reservation with the actual cost. Late commits (expired
|
|
109
|
+
* before commit landed) ARE accepted, transitioned to
|
|
110
|
+
* 'committed_post_expiry', charged to the budget, and surfaced via
|
|
111
|
+
* `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry`
|
|
112
|
+
* audit emit. See ADR-164.1 §5.3 + §8.1.
|
|
113
|
+
*/
|
|
114
|
+
commit(reservationId: string, actualUsd: number): CommitResult;
|
|
115
|
+
/**
|
|
116
|
+
* Release a reservation (caller decided not to proceed). State must be
|
|
117
|
+
* 'reserved'; any other state is ALREADY_FINALIZED.
|
|
118
|
+
*/
|
|
119
|
+
release(reservationId: string): ReleaseResult;
|
|
120
|
+
/**
|
|
121
|
+
* Sweep expired reservations: transition 'reserved' rows whose expiry has
|
|
122
|
+
* passed to 'expired'. Callers should drive this on a setInterval per
|
|
123
|
+
* ADR-164.1 §7.1 (default cadence 5s; this method does NOT install the
|
|
124
|
+
* timer — that's the integration site's responsibility).
|
|
125
|
+
* Returns the number of rows updated.
|
|
126
|
+
*/
|
|
127
|
+
sweepExpired(): number;
|
|
128
|
+
/**
|
|
129
|
+
* Diagnostic read: list reservations for a room. Not on the hot path.
|
|
130
|
+
*/
|
|
131
|
+
listReservations(roomId: string): Array<Record<string, unknown>>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Feature-flagged factory per ADR-164.1 §12.3. Returns an atomic tracker
|
|
135
|
+
* when `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`, otherwise returns null so the
|
|
136
|
+
* caller can keep using the file-based stub in pod-tick.mjs.
|
|
137
|
+
*/
|
|
138
|
+
export declare function createAtomicTrackerIfEnabled(opts: AtomicBbsRoomBudgetTrackerOptions): AtomicBbsRoomBudgetTracker | null;
|
|
139
|
+
//# sourceMappingURL=bbs-budget-tracker.d.ts.map
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BbsRoomBudgetTracker — Atomic reserve / commit / release token-bucket
|
|
3
|
+
* backed by better-sqlite3 (ADR-164.1 §3-§5).
|
|
4
|
+
*
|
|
5
|
+
* Closes the read-then-write race in ADR-164's first-draft tracker by routing
|
|
6
|
+
* every state transition through a single `BEGIN IMMEDIATE` transaction that
|
|
7
|
+
* acquires the SQLite write lock atomically with the gate check. The
|
|
8
|
+
* `_lock_bump` column write is a belt-and-suspenders explicit write inside
|
|
9
|
+
* the transaction so the lock acquisition is visible to code review and
|
|
10
|
+
* query-log analysis (§3.2 peer-review clarification).
|
|
11
|
+
*
|
|
12
|
+
* Architectural constraints (ADR-164.1):
|
|
13
|
+
* - Default OFF: this module is opt-in behind `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`.
|
|
14
|
+
* The pod-tick.mjs file-based stub remains the default until the flag is
|
|
15
|
+
* flipped per ADR-164.1 §12.4.
|
|
16
|
+
* - `committed_post_expiry` state machine implemented per §5.3 — late
|
|
17
|
+
* commits on expired reservations ARE accepted (the API spend already
|
|
18
|
+
* happened), transition to `committed_post_expiry`, charge the budget,
|
|
19
|
+
* and return `warned: 'COMMIT_AFTER_EXPIRY'`. This closes the §8.1
|
|
20
|
+
* Expired Commit Leak surfaced by peer review on 2026-06-29.
|
|
21
|
+
* - `expires_at` is clamped to [5_000, 300_000] ms (§3.2).
|
|
22
|
+
* - WAL + synchronous=NORMAL + busy_timeout=500 for write throughput.
|
|
23
|
+
*
|
|
24
|
+
* The sweeper interval (`sweepExpired()`) is exposed as a manual method;
|
|
25
|
+
* callers are expected to drive it on a setInterval — that integration lives
|
|
26
|
+
* at the call site, not in this file, per ADR-164.1 §7.1.
|
|
27
|
+
*
|
|
28
|
+
* @module @claude-flow/cli/business-pods/bbs-budget-tracker
|
|
29
|
+
*/
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
// ---------- expiry-window clamp -------------------------------------------
|
|
32
|
+
export const RESERVATION_EXPIRY_FLOOR_MS = 5_000;
|
|
33
|
+
export const RESERVATION_EXPIRY_CEILING_MS = 300_000;
|
|
34
|
+
export const RESERVATION_EXPIRY_DEFAULT_MS = 60_000;
|
|
35
|
+
export function clampReservationExpiry(raw) {
|
|
36
|
+
if (raw === undefined || !Number.isFinite(raw)) {
|
|
37
|
+
const envVal = Number(process.env.CLAUDE_FLOW_BBS_RESERVATION_EXPIRY_MS);
|
|
38
|
+
if (Number.isFinite(envVal) && envVal > 0) {
|
|
39
|
+
return Math.max(RESERVATION_EXPIRY_FLOOR_MS, Math.min(RESERVATION_EXPIRY_CEILING_MS, envVal));
|
|
40
|
+
}
|
|
41
|
+
return RESERVATION_EXPIRY_DEFAULT_MS;
|
|
42
|
+
}
|
|
43
|
+
return Math.max(RESERVATION_EXPIRY_FLOOR_MS, Math.min(RESERVATION_EXPIRY_CEILING_MS, raw));
|
|
44
|
+
}
|
|
45
|
+
// ---------- schema migration ----------------------------------------------
|
|
46
|
+
export const SCHEMA_SQL = `
|
|
47
|
+
PRAGMA journal_mode = WAL;
|
|
48
|
+
PRAGMA synchronous = NORMAL;
|
|
49
|
+
PRAGMA busy_timeout = 500;
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS bbs_budget_rooms (
|
|
52
|
+
room_id TEXT NOT NULL PRIMARY KEY,
|
|
53
|
+
monthly_cap_usd REAL NOT NULL CHECK (monthly_cap_usd >= 0),
|
|
54
|
+
billing_month TEXT NOT NULL,
|
|
55
|
+
_lock_bump INTEGER NOT NULL DEFAULT 0
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS bbs_budget_reservations (
|
|
59
|
+
reservation_id TEXT NOT NULL PRIMARY KEY,
|
|
60
|
+
room_id TEXT NOT NULL REFERENCES bbs_budget_rooms(room_id),
|
|
61
|
+
caller_node_id TEXT NOT NULL,
|
|
62
|
+
estimated_usd REAL NOT NULL CHECK (estimated_usd >= 0),
|
|
63
|
+
actual_usd REAL,
|
|
64
|
+
state TEXT NOT NULL
|
|
65
|
+
CHECK (state IN ('reserved','committed','released','expired','committed_post_expiry')),
|
|
66
|
+
reserved_at INTEGER NOT NULL,
|
|
67
|
+
expires_at INTEGER NOT NULL,
|
|
68
|
+
committed_at INTEGER,
|
|
69
|
+
audit_envelope_id TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_room_month
|
|
73
|
+
ON bbs_budget_reservations (room_id, reserved_at);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_expiry
|
|
76
|
+
ON bbs_budget_reservations (state, expires_at);
|
|
77
|
+
`;
|
|
78
|
+
// ---------- helpers --------------------------------------------------------
|
|
79
|
+
function currentBillingMonth(now) {
|
|
80
|
+
const d = new Date(now);
|
|
81
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
82
|
+
}
|
|
83
|
+
function billingMonthStartMs(billingMonth) {
|
|
84
|
+
const [y, m] = billingMonth.split('-').map((n) => Number(n));
|
|
85
|
+
return Date.UTC(y, (m - 1), 1, 0, 0, 0, 0);
|
|
86
|
+
}
|
|
87
|
+
const noopAudit = { emit: () => undefined };
|
|
88
|
+
export class AtomicBbsRoomBudgetTracker {
|
|
89
|
+
db;
|
|
90
|
+
defaultExpiryMs;
|
|
91
|
+
audit;
|
|
92
|
+
clock;
|
|
93
|
+
constructor(opts) {
|
|
94
|
+
this.db = opts.db;
|
|
95
|
+
this.defaultExpiryMs = clampReservationExpiry(opts.defaultExpiryMs);
|
|
96
|
+
this.audit = opts.audit ?? noopAudit;
|
|
97
|
+
this.clock = opts.clock ?? Date.now;
|
|
98
|
+
// Ensure schema is present. exec() is a no-op for tables that already
|
|
99
|
+
// exist due to IF NOT EXISTS.
|
|
100
|
+
this.db.exec(SCHEMA_SQL);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Register (or update) a room and its monthly cap. Idempotent.
|
|
104
|
+
*/
|
|
105
|
+
registerRoom(roomId, monthlyCapUsd) {
|
|
106
|
+
const billingMonth = currentBillingMonth(this.clock());
|
|
107
|
+
this.db
|
|
108
|
+
.prepare(`INSERT INTO bbs_budget_rooms (room_id, monthly_cap_usd, billing_month, _lock_bump)
|
|
109
|
+
VALUES (?, ?, ?, 0)
|
|
110
|
+
ON CONFLICT(room_id) DO UPDATE SET monthly_cap_usd = excluded.monthly_cap_usd`)
|
|
111
|
+
.run(roomId, monthlyCapUsd, billingMonth);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Atomically check budget and insert a reservation row in a single
|
|
115
|
+
* BEGIN IMMEDIATE transaction. See ADR-164.1 §5.2.
|
|
116
|
+
*/
|
|
117
|
+
reserve(roomId, callerId, estimatedUsd, opts) {
|
|
118
|
+
if (!Number.isFinite(estimatedUsd) || estimatedUsd < 0) {
|
|
119
|
+
throw new Error('estimatedUsd must be a non-negative finite number');
|
|
120
|
+
}
|
|
121
|
+
const auditEnvelopeId = opts?.auditEnvelopeId ?? `audit-${randomUUID()}`;
|
|
122
|
+
const expiryMs = clampReservationExpiry(opts?.expiryMs ?? this.defaultExpiryMs);
|
|
123
|
+
const nowMs = this.clock();
|
|
124
|
+
const billingMonth = currentBillingMonth(nowMs);
|
|
125
|
+
const billingMonthStart = billingMonthStartMs(billingMonth);
|
|
126
|
+
const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
|
|
127
|
+
const commitStmt = this.db.prepare('COMMIT');
|
|
128
|
+
const rollbackStmt = this.db.prepare('ROLLBACK');
|
|
129
|
+
beginStmt.run();
|
|
130
|
+
let transactionOpen = true;
|
|
131
|
+
try {
|
|
132
|
+
// Step 1: touch the room header row (the _lock_bump write makes the
|
|
133
|
+
// lock acquisition visible per §3.2 peer-review note).
|
|
134
|
+
const bumpRes = this.db
|
|
135
|
+
.prepare(`UPDATE bbs_budget_rooms
|
|
136
|
+
SET _lock_bump = _lock_bump + 1,
|
|
137
|
+
billing_month = CASE WHEN billing_month = ? THEN billing_month ELSE ? END
|
|
138
|
+
WHERE room_id = ?`)
|
|
139
|
+
.run(billingMonth, billingMonth, roomId);
|
|
140
|
+
if (bumpRes.changes === 0) {
|
|
141
|
+
rollbackStmt.run();
|
|
142
|
+
transactionOpen = false;
|
|
143
|
+
return { ok: false, error: 'ROOM_NOT_FOUND' };
|
|
144
|
+
}
|
|
145
|
+
const roomRow = this.db
|
|
146
|
+
.prepare(`SELECT monthly_cap_usd, billing_month FROM bbs_budget_rooms WHERE room_id = ?`)
|
|
147
|
+
.get(roomId);
|
|
148
|
+
if (!roomRow) {
|
|
149
|
+
rollbackStmt.run();
|
|
150
|
+
transactionOpen = false;
|
|
151
|
+
return { ok: false, error: 'ROOM_NOT_FOUND' };
|
|
152
|
+
}
|
|
153
|
+
const monthlyCap = roomRow.monthly_cap_usd;
|
|
154
|
+
// Step 3: compute committed + live-reserved totals within this billing month.
|
|
155
|
+
const totals = this.db
|
|
156
|
+
.prepare(`SELECT
|
|
157
|
+
COALESCE(SUM(CASE WHEN state IN ('committed','committed_post_expiry') THEN actual_usd ELSE 0 END), 0) AS committed_total,
|
|
158
|
+
COALESCE(SUM(CASE WHEN state = 'reserved' AND expires_at > ? THEN estimated_usd ELSE 0 END), 0) AS reserved_total
|
|
159
|
+
FROM bbs_budget_reservations
|
|
160
|
+
WHERE room_id = ? AND reserved_at >= ?`)
|
|
161
|
+
.get(nowMs, roomId, billingMonthStart);
|
|
162
|
+
const committedTotal = totals.committed_total ?? 0;
|
|
163
|
+
const reservedTotal = totals.reserved_total ?? 0;
|
|
164
|
+
const projected = committedTotal + reservedTotal + estimatedUsd;
|
|
165
|
+
if (projected > monthlyCap + 1e-9) {
|
|
166
|
+
rollbackStmt.run();
|
|
167
|
+
transactionOpen = false;
|
|
168
|
+
this.audit.emit('reservation.budget_exceeded', {
|
|
169
|
+
roomId, callerId, estimatedUsd, committedTotal, reservedTotal, monthlyCap,
|
|
170
|
+
});
|
|
171
|
+
return { ok: false, error: 'BUDGET_EXCEEDED' };
|
|
172
|
+
}
|
|
173
|
+
// Step 5: insert the reservation.
|
|
174
|
+
const reservationId = `res-${randomUUID()}`;
|
|
175
|
+
const expiresAt = nowMs + expiryMs;
|
|
176
|
+
this.db
|
|
177
|
+
.prepare(`INSERT INTO bbs_budget_reservations
|
|
178
|
+
(reservation_id, room_id, caller_node_id, estimated_usd, actual_usd,
|
|
179
|
+
state, reserved_at, expires_at, committed_at, audit_envelope_id)
|
|
180
|
+
VALUES (?, ?, ?, ?, NULL, 'reserved', ?, ?, NULL, ?)`)
|
|
181
|
+
.run(reservationId, roomId, callerId, estimatedUsd, nowMs, expiresAt, auditEnvelopeId);
|
|
182
|
+
commitStmt.run();
|
|
183
|
+
transactionOpen = false;
|
|
184
|
+
const remaining = monthlyCap - (committedTotal + reservedTotal + estimatedUsd);
|
|
185
|
+
this.audit.emit('reservation.reserved', {
|
|
186
|
+
reservationId, roomId, callerId, estimatedUsd, expiresAt, remaining,
|
|
187
|
+
});
|
|
188
|
+
return { ok: true, reservationId, remainingAfterReserve: remaining };
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (transactionOpen) {
|
|
192
|
+
try {
|
|
193
|
+
rollbackStmt.run();
|
|
194
|
+
}
|
|
195
|
+
catch { /* already-rolled */ }
|
|
196
|
+
}
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Commit the reservation with the actual cost. Late commits (expired
|
|
202
|
+
* before commit landed) ARE accepted, transitioned to
|
|
203
|
+
* 'committed_post_expiry', charged to the budget, and surfaced via
|
|
204
|
+
* `warned: 'COMMIT_AFTER_EXPIRY'` plus a `reservation.committed_post_expiry`
|
|
205
|
+
* audit emit. See ADR-164.1 §5.3 + §8.1.
|
|
206
|
+
*/
|
|
207
|
+
commit(reservationId, actualUsd) {
|
|
208
|
+
if (!Number.isFinite(actualUsd) || actualUsd < 0) {
|
|
209
|
+
throw new Error('actualUsd must be a non-negative finite number');
|
|
210
|
+
}
|
|
211
|
+
const nowMs = this.clock();
|
|
212
|
+
const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
|
|
213
|
+
const commitStmt = this.db.prepare('COMMIT');
|
|
214
|
+
const rollbackStmt = this.db.prepare('ROLLBACK');
|
|
215
|
+
beginStmt.run();
|
|
216
|
+
let transactionOpen = true;
|
|
217
|
+
try {
|
|
218
|
+
const row = this.db
|
|
219
|
+
.prepare(`SELECT state, room_id, estimated_usd, reserved_at, expires_at
|
|
220
|
+
FROM bbs_budget_reservations
|
|
221
|
+
WHERE reservation_id = ?`)
|
|
222
|
+
.get(reservationId);
|
|
223
|
+
if (!row) {
|
|
224
|
+
rollbackStmt.run();
|
|
225
|
+
transactionOpen = false;
|
|
226
|
+
return { ok: false, error: 'NOT_FOUND' };
|
|
227
|
+
}
|
|
228
|
+
if (row.state === 'committed' || row.state === 'committed_post_expiry' ||
|
|
229
|
+
row.state === 'released' || row.state === 'expired') {
|
|
230
|
+
rollbackStmt.run();
|
|
231
|
+
transactionOpen = false;
|
|
232
|
+
return { ok: false, error: 'ALREADY_FINALIZED' };
|
|
233
|
+
}
|
|
234
|
+
// state is 'reserved' here. Decide committed vs committed_post_expiry.
|
|
235
|
+
const lateCommit = row.expires_at <= nowMs;
|
|
236
|
+
const newState = lateCommit ? 'committed_post_expiry' : 'committed';
|
|
237
|
+
this.db
|
|
238
|
+
.prepare(`UPDATE bbs_budget_reservations
|
|
239
|
+
SET state = ?, actual_usd = ?, committed_at = ?
|
|
240
|
+
WHERE reservation_id = ?`)
|
|
241
|
+
.run(newState, actualUsd, nowMs, reservationId);
|
|
242
|
+
// Re-compute remaining for the return value.
|
|
243
|
+
const billingMonth = currentBillingMonth(nowMs);
|
|
244
|
+
const billingMonthStart = billingMonthStartMs(billingMonth);
|
|
245
|
+
const totals = this.db
|
|
246
|
+
.prepare(`SELECT
|
|
247
|
+
COALESCE(SUM(CASE WHEN state IN ('committed','committed_post_expiry') THEN actual_usd ELSE 0 END), 0) AS committed_total,
|
|
248
|
+
COALESCE(SUM(CASE WHEN state = 'reserved' AND expires_at > ? THEN estimated_usd ELSE 0 END), 0) AS reserved_total
|
|
249
|
+
FROM bbs_budget_reservations
|
|
250
|
+
WHERE room_id = ? AND reserved_at >= ?`)
|
|
251
|
+
.get(nowMs, row.room_id, billingMonthStart);
|
|
252
|
+
const roomRow = this.db
|
|
253
|
+
.prepare(`SELECT monthly_cap_usd FROM bbs_budget_rooms WHERE room_id = ?`)
|
|
254
|
+
.get(row.room_id);
|
|
255
|
+
const monthlyCap = roomRow?.monthly_cap_usd ?? 0;
|
|
256
|
+
const finalRemaining = monthlyCap - ((totals.committed_total ?? 0) + (totals.reserved_total ?? 0));
|
|
257
|
+
commitStmt.run();
|
|
258
|
+
transactionOpen = false;
|
|
259
|
+
if (lateCommit) {
|
|
260
|
+
// Emit the high-priority audit envelope per §8.1.
|
|
261
|
+
this.audit.emit('reservation.committed_post_expiry', {
|
|
262
|
+
reservationId, roomId: row.room_id, actualUsd, finalRemaining,
|
|
263
|
+
reservedAt: row.reserved_at, expiresAt: row.expires_at, committedAt: nowMs,
|
|
264
|
+
});
|
|
265
|
+
return { ok: true, warned: 'COMMIT_AFTER_EXPIRY', finalRemaining };
|
|
266
|
+
}
|
|
267
|
+
this.audit.emit('reservation.committed', {
|
|
268
|
+
reservationId, roomId: row.room_id, actualUsd, finalRemaining,
|
|
269
|
+
});
|
|
270
|
+
return { ok: true, committed: true, finalRemaining };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (transactionOpen) {
|
|
274
|
+
try {
|
|
275
|
+
rollbackStmt.run();
|
|
276
|
+
}
|
|
277
|
+
catch { /* already-rolled */ }
|
|
278
|
+
}
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Release a reservation (caller decided not to proceed). State must be
|
|
284
|
+
* 'reserved'; any other state is ALREADY_FINALIZED.
|
|
285
|
+
*/
|
|
286
|
+
release(reservationId) {
|
|
287
|
+
const beginStmt = this.db.prepare('BEGIN IMMEDIATE');
|
|
288
|
+
const commitStmt = this.db.prepare('COMMIT');
|
|
289
|
+
const rollbackStmt = this.db.prepare('ROLLBACK');
|
|
290
|
+
beginStmt.run();
|
|
291
|
+
let transactionOpen = true;
|
|
292
|
+
try {
|
|
293
|
+
const row = this.db
|
|
294
|
+
.prepare(`SELECT state, room_id FROM bbs_budget_reservations WHERE reservation_id = ?`)
|
|
295
|
+
.get(reservationId);
|
|
296
|
+
if (!row) {
|
|
297
|
+
rollbackStmt.run();
|
|
298
|
+
transactionOpen = false;
|
|
299
|
+
return { ok: false, error: 'NOT_FOUND' };
|
|
300
|
+
}
|
|
301
|
+
if (row.state !== 'reserved') {
|
|
302
|
+
rollbackStmt.run();
|
|
303
|
+
transactionOpen = false;
|
|
304
|
+
return { ok: false, error: 'ALREADY_FINALIZED' };
|
|
305
|
+
}
|
|
306
|
+
this.db
|
|
307
|
+
.prepare(`UPDATE bbs_budget_reservations SET state = 'released' WHERE reservation_id = ?`)
|
|
308
|
+
.run(reservationId);
|
|
309
|
+
commitStmt.run();
|
|
310
|
+
transactionOpen = false;
|
|
311
|
+
this.audit.emit('reservation.released', { reservationId, roomId: row.room_id });
|
|
312
|
+
return { ok: true, released: true };
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
if (transactionOpen) {
|
|
316
|
+
try {
|
|
317
|
+
rollbackStmt.run();
|
|
318
|
+
}
|
|
319
|
+
catch { /* already-rolled */ }
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Sweep expired reservations: transition 'reserved' rows whose expiry has
|
|
326
|
+
* passed to 'expired'. Callers should drive this on a setInterval per
|
|
327
|
+
* ADR-164.1 §7.1 (default cadence 5s; this method does NOT install the
|
|
328
|
+
* timer — that's the integration site's responsibility).
|
|
329
|
+
* Returns the number of rows updated.
|
|
330
|
+
*/
|
|
331
|
+
sweepExpired() {
|
|
332
|
+
const result = this.db
|
|
333
|
+
.prepare(`UPDATE bbs_budget_reservations
|
|
334
|
+
SET state = 'expired'
|
|
335
|
+
WHERE state = 'reserved' AND expires_at < ?`)
|
|
336
|
+
.run(this.clock());
|
|
337
|
+
return result.changes;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Diagnostic read: list reservations for a room. Not on the hot path.
|
|
341
|
+
*/
|
|
342
|
+
listReservations(roomId) {
|
|
343
|
+
return this.db
|
|
344
|
+
.prepare(`SELECT * FROM bbs_budget_reservations WHERE room_id = ? ORDER BY reserved_at`)
|
|
345
|
+
.all(roomId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Feature-flagged factory per ADR-164.1 §12.3. Returns an atomic tracker
|
|
350
|
+
* when `CLAUDE_FLOW_BBS_ATOMIC_BUDGET=1`, otherwise returns null so the
|
|
351
|
+
* caller can keep using the file-based stub in pod-tick.mjs.
|
|
352
|
+
*/
|
|
353
|
+
export function createAtomicTrackerIfEnabled(opts) {
|
|
354
|
+
if (process.env.CLAUDE_FLOW_BBS_ATOMIC_BUDGET !== '1')
|
|
355
|
+
return null;
|
|
356
|
+
return new AtomicBbsRoomBudgetTracker(opts);
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=bbs-budget-tracker.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-affinity routing policy — ADR-164 §3.4.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces a deterministic routing decision for `@metaharness/router` to use
|
|
5
|
+
* before its KRR cost-optimal step. The policy is intentionally tiny —
|
|
6
|
+
* three branches over `preferLocalExecution` and `budgetUsdMonthly` — so the
|
|
7
|
+
* decision is auditable from the pod template alone, no learned-weight side
|
|
8
|
+
* effects.
|
|
9
|
+
*
|
|
10
|
+
* Rules (per the Phase-3 brief):
|
|
11
|
+
* 1. `preferLocalExecution === true` → 'local-stdio'
|
|
12
|
+
* 2. `preferLocalExecution === false` AND `budgetUsdMonthly >= 50` → 'cloud-managed'
|
|
13
|
+
* 3. otherwise → 'remote-peer'
|
|
14
|
+
*
|
|
15
|
+
* The boundary `>= 50` matches the Phase-2 sales template (budget 50/month) —
|
|
16
|
+
* it routes the canonical reference pod to the cloud-managed backend. Pods
|
|
17
|
+
* below the threshold default to the federation peer-node backend.
|
|
18
|
+
*
|
|
19
|
+
* Wire point: `@metaharness/router` reads the result and supplies it as a
|
|
20
|
+
* routing hint *before* its KRR step. The router is free to override on
|
|
21
|
+
* latency/cost grounds — domain affinity is a policy floor, not a hard gate.
|
|
22
|
+
*
|
|
23
|
+
* @module @claude-flow/cli/business-pods/domain-affinity-policy
|
|
24
|
+
*/
|
|
25
|
+
import type { PodTemplate } from './pod-schema.js';
|
|
26
|
+
/** Three backends @metaharness/router can target for a pod tick. */
|
|
27
|
+
export type AgentBackend = 'local-stdio' | 'remote-peer' | 'cloud-managed';
|
|
28
|
+
/** Threshold for `cloud-managed` routing. Pods at or above this monthly
|
|
29
|
+
* spend default to cloud-managed when they are *not* local-pinned. */
|
|
30
|
+
export declare const CLOUD_BUDGET_THRESHOLD_USD = 50;
|
|
31
|
+
/** Structured routing decision. `reason` is rendered into the routing
|
|
32
|
+
* rationale via `hooks_explain` so operators can see why each pod was
|
|
33
|
+
* routed where it was. */
|
|
34
|
+
export interface BackendDecision {
|
|
35
|
+
backend: AgentBackend;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Decide which backend `@metaharness/router` should prefer for a pod tick.
|
|
40
|
+
*
|
|
41
|
+
* @param pod A validated PodTemplate. Validation is the caller's
|
|
42
|
+
* responsibility; this function will throw on malformed input
|
|
43
|
+
* (specifically on missing `preferLocalExecution` or
|
|
44
|
+
* `budgetUsdMonthly`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function selectAgentBackend(pod: PodTemplate): BackendDecision;
|
|
47
|
+
//# sourceMappingURL=domain-affinity-policy.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-affinity routing policy — ADR-164 §3.4.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces a deterministic routing decision for `@metaharness/router` to use
|
|
5
|
+
* before its KRR cost-optimal step. The policy is intentionally tiny —
|
|
6
|
+
* three branches over `preferLocalExecution` and `budgetUsdMonthly` — so the
|
|
7
|
+
* decision is auditable from the pod template alone, no learned-weight side
|
|
8
|
+
* effects.
|
|
9
|
+
*
|
|
10
|
+
* Rules (per the Phase-3 brief):
|
|
11
|
+
* 1. `preferLocalExecution === true` → 'local-stdio'
|
|
12
|
+
* 2. `preferLocalExecution === false` AND `budgetUsdMonthly >= 50` → 'cloud-managed'
|
|
13
|
+
* 3. otherwise → 'remote-peer'
|
|
14
|
+
*
|
|
15
|
+
* The boundary `>= 50` matches the Phase-2 sales template (budget 50/month) —
|
|
16
|
+
* it routes the canonical reference pod to the cloud-managed backend. Pods
|
|
17
|
+
* below the threshold default to the federation peer-node backend.
|
|
18
|
+
*
|
|
19
|
+
* Wire point: `@metaharness/router` reads the result and supplies it as a
|
|
20
|
+
* routing hint *before* its KRR step. The router is free to override on
|
|
21
|
+
* latency/cost grounds — domain affinity is a policy floor, not a hard gate.
|
|
22
|
+
*
|
|
23
|
+
* @module @claude-flow/cli/business-pods/domain-affinity-policy
|
|
24
|
+
*/
|
|
25
|
+
/** Threshold for `cloud-managed` routing. Pods at or above this monthly
|
|
26
|
+
* spend default to cloud-managed when they are *not* local-pinned. */
|
|
27
|
+
export const CLOUD_BUDGET_THRESHOLD_USD = 50;
|
|
28
|
+
/**
|
|
29
|
+
* Decide which backend `@metaharness/router` should prefer for a pod tick.
|
|
30
|
+
*
|
|
31
|
+
* @param pod A validated PodTemplate. Validation is the caller's
|
|
32
|
+
* responsibility; this function will throw on malformed input
|
|
33
|
+
* (specifically on missing `preferLocalExecution` or
|
|
34
|
+
* `budgetUsdMonthly`).
|
|
35
|
+
*/
|
|
36
|
+
export function selectAgentBackend(pod) {
|
|
37
|
+
if (pod === null || typeof pod !== 'object') {
|
|
38
|
+
throw new TypeError('selectAgentBackend: pod must be a validated PodTemplate object');
|
|
39
|
+
}
|
|
40
|
+
if (typeof pod.preferLocalExecution !== 'boolean') {
|
|
41
|
+
throw new TypeError('selectAgentBackend: pod.preferLocalExecution must be boolean (validate via pod-schema first)');
|
|
42
|
+
}
|
|
43
|
+
if (typeof pod.budgetUsdMonthly !== 'number' || !Number.isFinite(pod.budgetUsdMonthly)) {
|
|
44
|
+
throw new TypeError('selectAgentBackend: pod.budgetUsdMonthly must be a finite number (validate via pod-schema first)');
|
|
45
|
+
}
|
|
46
|
+
if (pod.preferLocalExecution) {
|
|
47
|
+
return {
|
|
48
|
+
backend: 'local-stdio',
|
|
49
|
+
reason: `pod "${pod.name}" preferLocalExecution=true — domain-affinity policy pins to local stdio`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (pod.budgetUsdMonthly >= CLOUD_BUDGET_THRESHOLD_USD) {
|
|
53
|
+
return {
|
|
54
|
+
backend: 'cloud-managed',
|
|
55
|
+
reason: `pod "${pod.name}" preferLocalExecution=false and budgetUsdMonthly=${pod.budgetUsdMonthly} ` +
|
|
56
|
+
`>= ${CLOUD_BUDGET_THRESHOLD_USD} — routes to cloud Managed Agents`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
backend: 'remote-peer',
|
|
61
|
+
reason: `pod "${pod.name}" preferLocalExecution=false and budgetUsdMonthly=${pod.budgetUsdMonthly} ` +
|
|
62
|
+
`< ${CLOUD_BUDGET_THRESHOLD_USD} — routes to federation peer node`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=domain-affinity-policy.js.map
|