sentinelayer-cli 0.27.1 → 0.28.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/README.md +1 -0
- package/package.json +1 -1
- package/src/ai/proxy.js +306 -15
- package/src/audit/persona-loop.js +3 -0
- package/src/commands/review.js +5 -0
- package/src/review/ai-review.js +10 -1
- package/src/session/daemon.js +5 -1
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ The `review` command can now add budget-governed AI reasoning on top of determin
|
|
|
259
259
|
|
|
260
260
|
- `sl review --ai --provider openai --model gpt-5.3-codex`
|
|
261
261
|
- `sl review --ai --ai-dry-run` (no provider call; deterministic synthetic output)
|
|
262
|
+
- `sl review --ai --require-usage-ledger` (fail closed if billing-grade usage cannot be recorded)
|
|
262
263
|
- `sl review --ai --max-cost 1.0 --max-tokens 0 --max-runtime-ms 0 --max-tool-calls 0`
|
|
263
264
|
|
|
264
265
|
AI artifacts are persisted in the same run folder:
|
package/package.json
CHANGED
package/src/ai/proxy.js
CHANGED
|
@@ -15,6 +15,298 @@ const DEFAULT_PROXY_MODEL = "gpt-5.3-codex";
|
|
|
15
15
|
const PROXY_TIMEOUT_MS = 120_000;
|
|
16
16
|
const PROXY_MAX_RETRIES = 2;
|
|
17
17
|
const PROXY_RETRY_STATUSES = new Set([429, 502, 503, 504]);
|
|
18
|
+
const ACTIONABLE_PROXY_DENIAL_CODES = new Set([
|
|
19
|
+
"FREE_TRIAL_EXPIRED",
|
|
20
|
+
"DAILY_SCAN_LIMIT_EXCEEDED",
|
|
21
|
+
"WEEKLY_SCAN_LIMIT_EXCEEDED",
|
|
22
|
+
"DAILY_BUDGET_EXCEEDED",
|
|
23
|
+
"RATE_LIMIT_EXCEEDED",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export class SentinelayerProxyError extends Error {
|
|
27
|
+
constructor(message, {
|
|
28
|
+
status = 0,
|
|
29
|
+
code = "PROXY_ERROR",
|
|
30
|
+
requestId = "",
|
|
31
|
+
retryAfterMs = undefined,
|
|
32
|
+
quota = null,
|
|
33
|
+
raw = null,
|
|
34
|
+
} = {}) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "SentinelayerProxyError";
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.requestId = requestId;
|
|
40
|
+
this.retryAfterMs = retryAfterMs;
|
|
41
|
+
this.quota = quota;
|
|
42
|
+
this.raw = raw;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serializeProxyError(error) {
|
|
47
|
+
if (!error || typeof error !== "object") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const status = normalizeInteger(error.status);
|
|
51
|
+
const code = String(error.code || "").trim();
|
|
52
|
+
const requestId = String(error.requestId || "").trim();
|
|
53
|
+
const retryAfterMs = normalizeInteger(error.retryAfterMs);
|
|
54
|
+
const quota =
|
|
55
|
+
error.quota && typeof error.quota === "object" && !Array.isArray(error.quota)
|
|
56
|
+
? JSON.parse(JSON.stringify(error.quota))
|
|
57
|
+
: null;
|
|
58
|
+
const serialized = {
|
|
59
|
+
status,
|
|
60
|
+
code: code || undefined,
|
|
61
|
+
requestId: requestId || undefined,
|
|
62
|
+
retryAfterMs,
|
|
63
|
+
quota,
|
|
64
|
+
};
|
|
65
|
+
for (const key of Object.keys(serialized)) {
|
|
66
|
+
if (serialized[key] === undefined || serialized[key] === null || serialized[key] === "") {
|
|
67
|
+
delete serialized[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(serialized).length > 0 ? serialized : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeNumber(value) {
|
|
74
|
+
if (value === undefined || value === null || value === "") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const numeric = Number(value);
|
|
78
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeInteger(value) {
|
|
82
|
+
const numeric = normalizeNumber(value);
|
|
83
|
+
return numeric === undefined ? undefined : Math.max(0, Math.floor(numeric));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getHeader(headers, name) {
|
|
87
|
+
if (!headers || typeof headers.get !== "function") {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return headers.get(name) || headers.get(name.toLowerCase()) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseRetryAfterMs(raw) {
|
|
94
|
+
if (typeof raw !== "string") {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const trimmed = raw.trim();
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const asSeconds = Number.parseFloat(trimmed);
|
|
102
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
103
|
+
return Math.ceil(asSeconds * 1000);
|
|
104
|
+
}
|
|
105
|
+
const asDate = Date.parse(trimmed);
|
|
106
|
+
if (Number.isNaN(asDate)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return Math.max(0, asDate - Date.now());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function chooseObject(...candidates) {
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeQuotaDetails(details = {}, headers = null) {
|
|
122
|
+
const policy = String(details.policy || "").trim();
|
|
123
|
+
const scope = String(details.scope || "").trim();
|
|
124
|
+
const unit = String(details.unit || "").trim();
|
|
125
|
+
const resetAt = String(details.resetAt || details.reset_at || "").trim();
|
|
126
|
+
const upgradeUrl = String(details.upgradeUrl || details.upgrade_url || "").trim();
|
|
127
|
+
const checkoutMode = String(details.checkoutMode || details.checkout_mode || "").trim();
|
|
128
|
+
const limit =
|
|
129
|
+
normalizeInteger(details.limit) ??
|
|
130
|
+
normalizeInteger(getHeader(headers, "X-RateLimit-Limit"));
|
|
131
|
+
const remaining =
|
|
132
|
+
normalizeInteger(details.remaining) ??
|
|
133
|
+
normalizeInteger(getHeader(headers, "X-RateLimit-Remaining"));
|
|
134
|
+
const used = normalizeInteger(details.used);
|
|
135
|
+
const resetAfterSeconds =
|
|
136
|
+
normalizeInteger(details.resetAfterSeconds ?? details.reset_after_seconds) ??
|
|
137
|
+
normalizeInteger(details.retryAfterSeconds ?? details.retry_after_seconds);
|
|
138
|
+
const retryAfterSeconds =
|
|
139
|
+
normalizeInteger(details.retryAfterSeconds ?? details.retry_after_seconds) ??
|
|
140
|
+
normalizeInteger(resetAfterSeconds);
|
|
141
|
+
const retryAfterMs =
|
|
142
|
+
retryAfterSeconds !== undefined
|
|
143
|
+
? retryAfterSeconds * 1000
|
|
144
|
+
: parseRetryAfterMs(getHeader(headers, "Retry-After"));
|
|
145
|
+
const headerReset = getHeader(headers, "X-RateLimit-Reset");
|
|
146
|
+
const headerPolicy = getHeader(headers, "X-RateLimit-Policy");
|
|
147
|
+
|
|
148
|
+
const quota = {
|
|
149
|
+
policy: policy || String(headerPolicy || "").trim() || undefined,
|
|
150
|
+
scope: scope || undefined,
|
|
151
|
+
limit,
|
|
152
|
+
remaining,
|
|
153
|
+
used,
|
|
154
|
+
unit: unit || undefined,
|
|
155
|
+
resetAfterSeconds,
|
|
156
|
+
resetAt: resetAt || String(headerReset || "").trim() || undefined,
|
|
157
|
+
retryAfterSeconds,
|
|
158
|
+
retryAfterMs,
|
|
159
|
+
upgradeUrl: upgradeUrl || undefined,
|
|
160
|
+
checkoutMode: checkoutMode || undefined,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
for (const key of Object.keys(quota)) {
|
|
164
|
+
if (quota[key] === undefined || quota[key] === "") {
|
|
165
|
+
delete quota[key];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Object.keys(quota).length > 0 ? quota : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatSeconds(seconds) {
|
|
173
|
+
const normalized = normalizeInteger(seconds);
|
|
174
|
+
if (normalized === undefined) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
if (normalized < 60) {
|
|
178
|
+
return `${normalized}s`;
|
|
179
|
+
}
|
|
180
|
+
const minutes = Math.ceil(normalized / 60);
|
|
181
|
+
if (minutes < 60) {
|
|
182
|
+
return `${minutes}m`;
|
|
183
|
+
}
|
|
184
|
+
const hours = Math.ceil(minutes / 60);
|
|
185
|
+
if (hours < 48) {
|
|
186
|
+
return `${hours}h`;
|
|
187
|
+
}
|
|
188
|
+
const days = Math.ceil(hours / 24);
|
|
189
|
+
return `${days}d`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildProxyErrorMessage({ status, code, message, quota }) {
|
|
193
|
+
const parts = [`SentinelLayer LLM proxy error (${status}${code ? ` ${code}` : ""}): ${message || "Request denied"}`];
|
|
194
|
+
if (quota?.policy || quota?.scope) {
|
|
195
|
+
parts.push(`policy=${quota.policy || "unknown"}${quota.scope ? ` scope=${quota.scope}` : ""}`);
|
|
196
|
+
}
|
|
197
|
+
if (quota?.limit !== undefined || quota?.remaining !== undefined || quota?.used !== undefined) {
|
|
198
|
+
const unit = quota.unit ? ` ${quota.unit}` : "";
|
|
199
|
+
const limit = quota.limit !== undefined ? quota.limit : "?";
|
|
200
|
+
const remaining = quota.remaining !== undefined ? quota.remaining : "?";
|
|
201
|
+
const used = quota.used !== undefined ? quota.used : "?";
|
|
202
|
+
parts.push(`quota used=${used} remaining=${remaining} limit=${limit}${unit}`);
|
|
203
|
+
}
|
|
204
|
+
if (quota?.resetAt) {
|
|
205
|
+
parts.push(`resets at ${quota.resetAt}`);
|
|
206
|
+
} else if (quota?.resetAfterSeconds !== undefined) {
|
|
207
|
+
parts.push(`resets in ${formatSeconds(quota.resetAfterSeconds)}`);
|
|
208
|
+
} else if (quota?.retryAfterSeconds !== undefined) {
|
|
209
|
+
parts.push(`retry after ${formatSeconds(quota.retryAfterSeconds)}`);
|
|
210
|
+
}
|
|
211
|
+
if (quota?.upgradeUrl) {
|
|
212
|
+
parts.push(`upgrade: ${quota.upgradeUrl}`);
|
|
213
|
+
}
|
|
214
|
+
if (quota?.checkoutMode) {
|
|
215
|
+
parts.push(`checkout=${quota.checkoutMode}`);
|
|
216
|
+
}
|
|
217
|
+
return parts.join(" | ");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function parseProxyErrorResponse(response) {
|
|
221
|
+
let payload = null;
|
|
222
|
+
let text = "";
|
|
223
|
+
try {
|
|
224
|
+
payload = await response.json();
|
|
225
|
+
} catch {
|
|
226
|
+
text = await response.text().catch(() => "");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const detailObject =
|
|
230
|
+
payload?.detail && typeof payload.detail === "object" && !Array.isArray(payload.detail)
|
|
231
|
+
? payload.detail
|
|
232
|
+
: null;
|
|
233
|
+
const nestedError = chooseObject(payload?.error, detailObject?.error);
|
|
234
|
+
const details = chooseObject(
|
|
235
|
+
nestedError?.details,
|
|
236
|
+
nestedError?.detail,
|
|
237
|
+
payload?.details,
|
|
238
|
+
detailObject?.details,
|
|
239
|
+
detailObject
|
|
240
|
+
);
|
|
241
|
+
const code = String(
|
|
242
|
+
nestedError?.code ||
|
|
243
|
+
payload?.code ||
|
|
244
|
+
detailObject?.code ||
|
|
245
|
+
"PROXY_ERROR"
|
|
246
|
+
).trim() || "PROXY_ERROR";
|
|
247
|
+
const message = String(
|
|
248
|
+
nestedError?.message ||
|
|
249
|
+
payload?.message ||
|
|
250
|
+
(typeof payload?.detail === "string" ? payload.detail : "") ||
|
|
251
|
+
detailObject?.message ||
|
|
252
|
+
text ||
|
|
253
|
+
"Request denied"
|
|
254
|
+
).trim();
|
|
255
|
+
const requestId = String(
|
|
256
|
+
nestedError?.request_id ||
|
|
257
|
+
nestedError?.requestId ||
|
|
258
|
+
payload?.request_id ||
|
|
259
|
+
payload?.requestId ||
|
|
260
|
+
detailObject?.request_id ||
|
|
261
|
+
detailObject?.requestId ||
|
|
262
|
+
getHeader(response.headers, "x-request-id") ||
|
|
263
|
+
""
|
|
264
|
+
).trim();
|
|
265
|
+
const quota = normalizeQuotaDetails(details, response.headers);
|
|
266
|
+
const retryAfterMs =
|
|
267
|
+
quota?.retryAfterMs ??
|
|
268
|
+
parseRetryAfterMs(getHeader(response.headers, "Retry-After")) ??
|
|
269
|
+
normalizeInteger(nestedError?.retry_after_ms ?? payload?.retry_after_ms);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
code,
|
|
273
|
+
message,
|
|
274
|
+
requestId,
|
|
275
|
+
quota,
|
|
276
|
+
retryAfterMs,
|
|
277
|
+
raw: payload || text,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isActionableProxyDenial(status, parsed) {
|
|
282
|
+
if (status === 402 || status === 403) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (ACTIONABLE_PROXY_DENIAL_CODES.has(String(parsed?.code || "").trim())) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return Boolean(parsed?.quota?.policy || parsed?.quota?.upgradeUrl || parsed?.quota?.resetAt);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildProxyError(status, parsed) {
|
|
292
|
+
const retryAfterMs = parsed.retryAfterMs ?? parsed.quota?.retryAfterMs;
|
|
293
|
+
return new SentinelayerProxyError(
|
|
294
|
+
buildProxyErrorMessage({
|
|
295
|
+
status,
|
|
296
|
+
code: parsed.code,
|
|
297
|
+
message: parsed.message,
|
|
298
|
+
quota: parsed.quota,
|
|
299
|
+
}),
|
|
300
|
+
{
|
|
301
|
+
status,
|
|
302
|
+
code: parsed.code,
|
|
303
|
+
requestId: parsed.requestId,
|
|
304
|
+
retryAfterMs,
|
|
305
|
+
quota: parsed.quota,
|
|
306
|
+
raw: parsed.raw,
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
}
|
|
18
310
|
|
|
19
311
|
/**
|
|
20
312
|
* Invoke LLM via SentinelLayer proxy.
|
|
@@ -128,15 +420,25 @@ export async function invokeViaProxy({
|
|
|
128
420
|
clearTimeout(timeoutHandle);
|
|
129
421
|
}
|
|
130
422
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
423
|
+
if (response.ok) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const parsedError = await parseProxyErrorResponse(response);
|
|
428
|
+
const hardDenial = isActionableProxyDenial(response.status, parsedError);
|
|
429
|
+
if (!hardDenial && PROXY_RETRY_STATUSES.has(response.status) && attempt < PROXY_MAX_RETRIES) {
|
|
430
|
+
const retryAfter = getHeader(response.headers, "Retry-After");
|
|
431
|
+
const retryAfterSeconds = retryAfter && !Number.isNaN(Number(retryAfter)) ? Number(retryAfter) : null;
|
|
432
|
+
const delay = retryAfterSeconds !== null ? Math.min(retryAfterSeconds, 5) * 1000 : 500 * (attempt + 1);
|
|
134
433
|
await new Promise((r) => setTimeout(r, delay));
|
|
135
434
|
continue;
|
|
136
435
|
}
|
|
137
436
|
|
|
138
|
-
|
|
437
|
+
throw buildProxyError(response.status, parsedError);
|
|
139
438
|
} catch (err) {
|
|
439
|
+
if (err instanceof SentinelayerProxyError) {
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
140
442
|
lastError = err;
|
|
141
443
|
if (attempt >= PROXY_MAX_RETRIES) break;
|
|
142
444
|
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
@@ -147,17 +449,6 @@ export async function invokeViaProxy({
|
|
|
147
449
|
throw new Error(`SentinelLayer LLM proxy request failed: ${lastError?.message || "no response"}`);
|
|
148
450
|
}
|
|
149
451
|
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
let detail = "";
|
|
152
|
-
try {
|
|
153
|
-
const errBody = await response.json();
|
|
154
|
-
detail = errBody?.error?.message || errBody?.detail || JSON.stringify(errBody).slice(0, 200);
|
|
155
|
-
} catch {
|
|
156
|
-
detail = await response.text().catch(() => "");
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`SentinelLayer LLM proxy error (${response.status}): ${detail}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
452
|
const result = await response.json();
|
|
162
453
|
|
|
163
454
|
return {
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { createMultiProviderApiClient } from "../ai/client.js";
|
|
5
|
+
import { serializeProxyError } from "../ai/proxy.js";
|
|
5
6
|
import { evaluateBudget } from "../cost/budget.js";
|
|
6
7
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
7
8
|
import { createAgentEvent } from "../events/schema.js";
|
|
@@ -866,9 +867,11 @@ async function runSinglePersonaAgenticLoop({
|
|
|
866
867
|
});
|
|
867
868
|
} catch (error) {
|
|
868
869
|
status = "llm_error_fallback";
|
|
870
|
+
const proxyError = serializeProxyError(error);
|
|
869
871
|
emit("llm_error", {
|
|
870
872
|
turn: turnCount,
|
|
871
873
|
error: error instanceof Error ? error.message : String(error),
|
|
874
|
+
...(proxyError ? { proxyError } : {}),
|
|
872
875
|
});
|
|
873
876
|
break;
|
|
874
877
|
}
|
package/src/commands/review.js
CHANGED
|
@@ -120,6 +120,7 @@ async function executeReviewRun({
|
|
|
120
120
|
maxNoProgress: aiConfig.maxNoProgress,
|
|
121
121
|
warningThresholdPercent: aiConfig.warnAtPercent,
|
|
122
122
|
dryRun: Boolean(aiConfig.aiDryRun),
|
|
123
|
+
requireUsageLedger: Boolean(aiConfig.requireUsageLedger),
|
|
123
124
|
env: process.env,
|
|
124
125
|
});
|
|
125
126
|
}
|
|
@@ -146,6 +147,7 @@ async function executeReviewRun({
|
|
|
146
147
|
invocation: {
|
|
147
148
|
aiEnabled: Boolean(aiConfig.enable),
|
|
148
149
|
aiDryRun: Boolean(aiConfig.aiDryRun),
|
|
150
|
+
requireUsageLedger: Boolean(aiConfig.requireUsageLedger),
|
|
149
151
|
provider: aiConfig.provider,
|
|
150
152
|
model: aiConfig.model,
|
|
151
153
|
sessionId: aiConfig.sessionId,
|
|
@@ -209,6 +211,7 @@ async function executeReviewRun({
|
|
|
209
211
|
budget: aiLayer.budget,
|
|
210
212
|
cost: aiLayer.cost,
|
|
211
213
|
telemetry: aiLayer.telemetry,
|
|
214
|
+
billing: aiLayer.billing,
|
|
212
215
|
}
|
|
213
216
|
: {
|
|
214
217
|
enabled: false,
|
|
@@ -283,6 +286,7 @@ export function registerReviewCommand(program) {
|
|
|
283
286
|
.option("--staged", "Alias for --mode staged")
|
|
284
287
|
.option("--ai", "Enable AI reasoning layer over deterministic findings")
|
|
285
288
|
.option("--ai-dry-run", "Run AI layer in dry-run mode (no provider call)")
|
|
289
|
+
.option("--require-usage-ledger", "Fail --ai review if billing-grade session_usage cannot be recorded")
|
|
286
290
|
.option("--provider <name>", "AI provider override (openai|anthropic|google)")
|
|
287
291
|
.option("--model <id>", "AI model override")
|
|
288
292
|
.option("--api-key <key>", "Optional explicit API key override")
|
|
@@ -314,6 +318,7 @@ export function registerReviewCommand(program) {
|
|
|
314
318
|
aiConfig: {
|
|
315
319
|
enable: Boolean(options.ai),
|
|
316
320
|
aiDryRun: Boolean(options.aiDryRun),
|
|
321
|
+
requireUsageLedger: Boolean(options.requireUsageLedger),
|
|
317
322
|
provider: options.provider,
|
|
318
323
|
model: options.model,
|
|
319
324
|
apiKey: options.apiKey,
|
package/src/review/ai-review.js
CHANGED
|
@@ -486,6 +486,8 @@ export async function runAiReviewLayer({
|
|
|
486
486
|
warningThresholdPercent = 80,
|
|
487
487
|
systemPrompt = "",
|
|
488
488
|
dryRun = false,
|
|
489
|
+
requireUsageLedger = false,
|
|
490
|
+
usageRecorder = recordSessionUsage,
|
|
489
491
|
env = process.env,
|
|
490
492
|
} = {}) {
|
|
491
493
|
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
@@ -496,6 +498,7 @@ export async function runAiReviewLayer({
|
|
|
496
498
|
Math.floor(Number(maxFindings || DEFAULT_AI_MAX_FINDINGS))
|
|
497
499
|
);
|
|
498
500
|
const normalizedRunId = normalizeString(runId) || "review-ai";
|
|
501
|
+
const normalizedRequireUsageLedger = Boolean(requireUsageLedger);
|
|
499
502
|
|
|
500
503
|
const config = await loadConfig({ cwd: normalizedTargetPath, env });
|
|
501
504
|
let resolvedProvider = resolveProvider({
|
|
@@ -663,7 +666,7 @@ export async function runAiReviewLayer({
|
|
|
663
666
|
dryRun: Boolean(dryRun),
|
|
664
667
|
}),
|
|
665
668
|
});
|
|
666
|
-
sessionUsageLedger = await
|
|
669
|
+
sessionUsageLedger = await usageRecorder(
|
|
667
670
|
normalizedSessionId,
|
|
668
671
|
{
|
|
669
672
|
agentId: "audit-orchestrator",
|
|
@@ -693,6 +696,11 @@ export async function runAiReviewLayer({
|
|
|
693
696
|
ok: false,
|
|
694
697
|
reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
|
|
695
698
|
};
|
|
699
|
+
if (normalizedRequireUsageLedger) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Review AI usage ledger recording failed: ${sessionUsageLedger.reason || "unknown"}.`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
696
704
|
}
|
|
697
705
|
|
|
698
706
|
let stopTelemetry = null;
|
|
@@ -754,6 +762,7 @@ export async function runAiReviewLayer({
|
|
|
754
762
|
provider: resolvedProvider,
|
|
755
763
|
model: resolvedModel,
|
|
756
764
|
dryRun: Boolean(dryRun),
|
|
765
|
+
requireUsageLedger: normalizedRequireUsageLedger,
|
|
757
766
|
usage,
|
|
758
767
|
pricingFound: modelCost.pricingFound,
|
|
759
768
|
sessionUsageLedger,
|
package/src/session/daemon.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import fsp from "node:fs/promises";
|
|
4
4
|
|
|
5
|
-
import { invokeViaProxy } from "../ai/proxy.js";
|
|
5
|
+
import { invokeViaProxy, serializeProxyError } from "../ai/proxy.js";
|
|
6
6
|
import { createAgentEvent } from "../events/schema.js";
|
|
7
7
|
import {
|
|
8
8
|
buildDocumentsFromBlackboardEntries,
|
|
@@ -496,6 +496,7 @@ async function buildHelpResponseMessage(
|
|
|
496
496
|
let llmText = "";
|
|
497
497
|
let fallbackPath = false;
|
|
498
498
|
let fallbackReason = "";
|
|
499
|
+
let proxyError = null;
|
|
499
500
|
let usage = {
|
|
500
501
|
inputTokens: 0,
|
|
501
502
|
outputTokens: 0,
|
|
@@ -556,6 +557,7 @@ async function buildHelpResponseMessage(
|
|
|
556
557
|
} catch (error) {
|
|
557
558
|
fallbackPath = true;
|
|
558
559
|
fallbackReason = normalizeString(error?.message || error) || "Senti model invocation failed.";
|
|
560
|
+
proxyError = serializeProxyError(error);
|
|
559
561
|
}
|
|
560
562
|
|
|
561
563
|
if (!usage.latencyMs) {
|
|
@@ -579,6 +581,7 @@ async function buildHelpResponseMessage(
|
|
|
579
581
|
usage,
|
|
580
582
|
fallbackPath,
|
|
581
583
|
fallbackReason,
|
|
584
|
+
proxyError,
|
|
582
585
|
contextSignals: {
|
|
583
586
|
documentCount: documents.length,
|
|
584
587
|
memoryHits: memoryHits.length,
|
|
@@ -641,6 +644,7 @@ async function maybeRespondToHelpRequest(
|
|
|
641
644
|
latencyMs: response.usage.latencyMs,
|
|
642
645
|
fallbackPath: Boolean(response.fallbackPath),
|
|
643
646
|
fallbackReason: response.fallbackReason || null,
|
|
647
|
+
proxyError: response.proxyError || null,
|
|
644
648
|
contextSignals: response.contextSignals,
|
|
645
649
|
},
|
|
646
650
|
{
|