sentinelayer-cli 0.28.4 → 0.29.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/src/commands/omargate.js +2 -0
- package/src/commands/session.js +251 -61
- package/src/legacy-cli.js +14 -0
- package/src/mcp/session-stdio-server.js +106 -7
- package/src/review/ai-review.js +24 -9
- package/src/review/omargate-orchestrator.js +95 -1
package/package.json
CHANGED
package/src/commands/omargate.js
CHANGED
|
@@ -20,6 +20,8 @@ export function registerOmarGateCommand(program, invokeLegacy) {
|
|
|
20
20
|
.option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
|
|
21
21
|
.option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
|
|
22
22
|
.option("--max-cost <usd>", "Maximum AI layer cost in USD (default: 5.0)")
|
|
23
|
+
.option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session")
|
|
24
|
+
.option("--require-usage-ledger", "Fail if Omar Deep AI calls cannot write billing-grade session_usage")
|
|
23
25
|
.option("--persona <csv>", "Only run these personas (comma-separated IDs); unknown IDs are dropped + warned")
|
|
24
26
|
.option("--skip-persona <csv>", "Skip these personas (comma-separated IDs)")
|
|
25
27
|
.option("--stream", "Emit NDJSON events to stdout as personas run")
|
package/src/commands/session.js
CHANGED
|
@@ -140,6 +140,8 @@ const SESSION_SAY_CONFIRM_ATTEMPTS = 3;
|
|
|
140
140
|
const SESSION_SAY_CONFIRM_DELAY_MS = 250;
|
|
141
141
|
const SESSION_SAY_CONFIRM_PAGE_LIMIT = 200;
|
|
142
142
|
const SESSION_SAY_CONFIRM_MAX_PAGES = 10;
|
|
143
|
+
const SESSION_SAY_CONFIRM_TOTAL_TIMEOUT_MS = 10_000;
|
|
144
|
+
const SESSION_SAY_CONFIRM_REQUEST_TIMEOUT_MS = 2_000;
|
|
143
145
|
const SESSION_SAY_LOCAL_ONLY_REASONS = new Set([
|
|
144
146
|
"no_session",
|
|
145
147
|
"not_authenticated",
|
|
@@ -201,21 +203,131 @@ async function readSessionConfirmationAnchor(sessionId, { targetPath } = {}) {
|
|
|
201
203
|
};
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
function normalizePositiveIntegerOrDefault(value, fallbackValue) {
|
|
207
|
+
const normalized = Number(value);
|
|
208
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
209
|
+
return fallbackValue;
|
|
210
|
+
}
|
|
211
|
+
return Math.floor(normalized);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function confirmationRemainingMs(deadlineMs, nowMs) {
|
|
215
|
+
const now = Number(nowMs()) || Date.now();
|
|
216
|
+
return Math.max(0, Math.floor(deadlineMs - now));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function confirmSessionEventVisible(
|
|
220
|
+
sessionId,
|
|
221
|
+
clientMessageId,
|
|
222
|
+
{
|
|
223
|
+
targetPath,
|
|
224
|
+
anchorCursor = null,
|
|
225
|
+
pollSessionEventsFn = pollSessionEvents,
|
|
226
|
+
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
227
|
+
sleepFn = sleep,
|
|
228
|
+
nowMs = Date.now,
|
|
229
|
+
attempts = SESSION_SAY_CONFIRM_ATTEMPTS,
|
|
230
|
+
delayMs = SESSION_SAY_CONFIRM_DELAY_MS,
|
|
231
|
+
pageLimit = SESSION_SAY_CONFIRM_PAGE_LIMIT,
|
|
232
|
+
maxPages = SESSION_SAY_CONFIRM_MAX_PAGES,
|
|
233
|
+
totalTimeoutMs = SESSION_SAY_CONFIRM_TOTAL_TIMEOUT_MS,
|
|
234
|
+
requestTimeoutMs = SESSION_SAY_CONFIRM_REQUEST_TIMEOUT_MS,
|
|
235
|
+
} = {},
|
|
236
|
+
) {
|
|
205
237
|
let lastReason = "not_visible";
|
|
206
238
|
let checked = 0;
|
|
207
239
|
let pages = 0;
|
|
240
|
+
let tailChecks = 0;
|
|
208
241
|
const normalizedAnchorCursor = normalizeString(anchorCursor) || null;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
242
|
+
const normalizedAttempts = normalizePositiveIntegerOrDefault(attempts, SESSION_SAY_CONFIRM_ATTEMPTS);
|
|
243
|
+
const normalizedDelayMs = normalizePositiveIntegerOrDefault(delayMs, SESSION_SAY_CONFIRM_DELAY_MS);
|
|
244
|
+
const normalizedPageLimit = normalizePositiveIntegerOrDefault(pageLimit, SESSION_SAY_CONFIRM_PAGE_LIMIT);
|
|
245
|
+
const normalizedMaxPages = normalizePositiveIntegerOrDefault(maxPages, SESSION_SAY_CONFIRM_MAX_PAGES);
|
|
246
|
+
const normalizedTotalTimeoutMs = normalizePositiveIntegerOrDefault(
|
|
247
|
+
totalTimeoutMs,
|
|
248
|
+
SESSION_SAY_CONFIRM_TOTAL_TIMEOUT_MS,
|
|
249
|
+
);
|
|
250
|
+
const normalizedRequestTimeoutMs = normalizePositiveIntegerOrDefault(
|
|
251
|
+
requestTimeoutMs,
|
|
252
|
+
SESSION_SAY_CONFIRM_REQUEST_TIMEOUT_MS,
|
|
253
|
+
);
|
|
254
|
+
const deadlineMs = (Number(nowMs()) || Date.now()) + normalizedTotalTimeoutMs;
|
|
255
|
+
const nextRequestTimeoutMs = () => {
|
|
256
|
+
const remaining = confirmationRemainingMs(deadlineMs, nowMs);
|
|
257
|
+
if (remaining <= 0) return 0;
|
|
258
|
+
return Math.max(1, Math.min(normalizedRequestTimeoutMs, remaining));
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const maybeConfirmFromLatestTail = async () => {
|
|
262
|
+
const timeoutMs = nextRequestTimeoutMs();
|
|
263
|
+
if (timeoutMs <= 0) {
|
|
264
|
+
lastReason = "confirmation_timeout";
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
tailChecks += 1;
|
|
268
|
+
let result = null;
|
|
269
|
+
try {
|
|
270
|
+
result = await pollSessionEventsBeforeFn(sessionId, {
|
|
214
271
|
targetPath,
|
|
215
|
-
|
|
216
|
-
|
|
272
|
+
limit: normalizedPageLimit,
|
|
273
|
+
timeoutMs,
|
|
217
274
|
forceCircuitProbe: true,
|
|
218
275
|
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
lastReason = normalizeString(error?.code || error?.message) || "confirmation_tail_poll_failed";
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!result?.ok) {
|
|
281
|
+
lastReason = normalizeString(result?.reason) || "confirmation_tail_poll_failed";
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
285
|
+
checked += events.length;
|
|
286
|
+
const confirmedEvent = events.find((candidate) => sessionEventMatchesClientMessageId(candidate, clientMessageId));
|
|
287
|
+
if (!confirmedEvent) {
|
|
288
|
+
lastReason = "not_visible";
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
confirmed: true,
|
|
293
|
+
reason: "",
|
|
294
|
+
checked,
|
|
295
|
+
pages,
|
|
296
|
+
tailChecks,
|
|
297
|
+
source: "latest_tail",
|
|
298
|
+
anchorCursor: normalizedAnchorCursor,
|
|
299
|
+
event: confirmedEvent,
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
for (let attempt = 1; attempt <= normalizedAttempts; attempt += 1) {
|
|
304
|
+
let pageCursor = normalizedAnchorCursor;
|
|
305
|
+
for (let page = 1; page <= normalizedMaxPages; page += 1) {
|
|
306
|
+
const timeoutMs = nextRequestTimeoutMs();
|
|
307
|
+
if (timeoutMs <= 0) {
|
|
308
|
+
return {
|
|
309
|
+
confirmed: false,
|
|
310
|
+
reason: "confirmation_timeout",
|
|
311
|
+
checked,
|
|
312
|
+
pages,
|
|
313
|
+
tailChecks,
|
|
314
|
+
anchorCursor: normalizedAnchorCursor,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
pages += 1;
|
|
318
|
+
let result = null;
|
|
319
|
+
try {
|
|
320
|
+
result = await pollSessionEventsFn(sessionId, {
|
|
321
|
+
targetPath,
|
|
322
|
+
since: pageCursor,
|
|
323
|
+
limit: normalizedPageLimit,
|
|
324
|
+
timeoutMs,
|
|
325
|
+
forceCircuitProbe: true,
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
lastReason = normalizeString(error?.code || error?.message) || "confirmation_poll_failed";
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
219
331
|
if (!result?.ok) {
|
|
220
332
|
lastReason = normalizeString(result?.reason) || "confirmation_poll_failed";
|
|
221
333
|
break;
|
|
@@ -229,6 +341,8 @@ async function confirmSessionEventVisible(sessionId, clientMessageId, { targetPa
|
|
|
229
341
|
reason: "",
|
|
230
342
|
checked,
|
|
231
343
|
pages,
|
|
344
|
+
tailChecks,
|
|
345
|
+
source: "forward_cursor",
|
|
232
346
|
anchorCursor: normalizedAnchorCursor,
|
|
233
347
|
event: confirmedEvent,
|
|
234
348
|
};
|
|
@@ -240,8 +354,17 @@ async function confirmSessionEventVisible(sessionId, clientMessageId, { targetPa
|
|
|
240
354
|
}
|
|
241
355
|
pageCursor = nextCursor;
|
|
242
356
|
}
|
|
243
|
-
|
|
244
|
-
|
|
357
|
+
const tailConfirmation = await maybeConfirmFromLatestTail();
|
|
358
|
+
if (tailConfirmation) {
|
|
359
|
+
return tailConfirmation;
|
|
360
|
+
}
|
|
361
|
+
if (attempt < normalizedAttempts) {
|
|
362
|
+
const remaining = confirmationRemainingMs(deadlineMs, nowMs);
|
|
363
|
+
if (remaining <= 0) {
|
|
364
|
+
lastReason = "confirmation_timeout";
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
await sleepFn(Math.min(normalizedDelayMs, remaining));
|
|
245
368
|
}
|
|
246
369
|
}
|
|
247
370
|
return {
|
|
@@ -249,6 +372,7 @@ async function confirmSessionEventVisible(sessionId, clientMessageId, { targetPa
|
|
|
249
372
|
reason: lastReason,
|
|
250
373
|
checked,
|
|
251
374
|
pages,
|
|
375
|
+
tailChecks,
|
|
252
376
|
anchorCursor: normalizedAnchorCursor,
|
|
253
377
|
};
|
|
254
378
|
}
|
|
@@ -1058,6 +1182,44 @@ async function findReusableSessionCandidate({
|
|
|
1058
1182
|
// - When `SENTINELAYER_SKIP_REMOTE_SYNC=1` (test bootstrap), short-circuits
|
|
1059
1183
|
// to `{ ok: true, source: "skipped", session: null }` so unit tests
|
|
1060
1184
|
// can exercise the local materialization path without a real API.
|
|
1185
|
+
async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
1186
|
+
let response;
|
|
1187
|
+
try {
|
|
1188
|
+
response = await fetch(endpoint, { method: "GET", headers });
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
return {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
response: null,
|
|
1193
|
+
status: 0,
|
|
1194
|
+
reason: normalizeString(err?.message) || "fetch_failed",
|
|
1195
|
+
retryable: true,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
if (response && response.ok) {
|
|
1199
|
+
return { ok: true, response, status: response.status, reason: "", retryable: false };
|
|
1200
|
+
}
|
|
1201
|
+
if (!response) {
|
|
1202
|
+
return { ok: false, response: null, status: 0, reason: "no_response", retryable: true };
|
|
1203
|
+
}
|
|
1204
|
+
const status = Number(response.status) || 0;
|
|
1205
|
+
return {
|
|
1206
|
+
ok: false,
|
|
1207
|
+
response,
|
|
1208
|
+
status,
|
|
1209
|
+
reason: `api_${status || "unknown"}`,
|
|
1210
|
+
retryable: status >= 500 && status < 600,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function parseRemoteSessionDetailResponse(response) {
|
|
1215
|
+
const body = await response.json().catch(() => ({}));
|
|
1216
|
+
return body && body.session && typeof body.session === "object"
|
|
1217
|
+
? body.session
|
|
1218
|
+
: body && typeof body === "object"
|
|
1219
|
+
? body
|
|
1220
|
+
: null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1061
1223
|
async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
1062
1224
|
const normalizedSessionId = normalizeString(sessionId);
|
|
1063
1225
|
if (!normalizedSessionId) {
|
|
@@ -1085,60 +1247,43 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1085
1247
|
}
|
|
1086
1248
|
const endpoint = `${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}`;
|
|
1087
1249
|
const headers = { Authorization: `Bearer ${auth.token}` };
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
if (response.status === 404) {
|
|
1116
|
-
// Pre-#483 fallback: scan the list endpoint once for the same id.
|
|
1117
|
-
const listResult = await listSessionsFromApi({
|
|
1118
|
-
targetPath,
|
|
1119
|
-
includeArchived: false,
|
|
1120
|
-
limit: 50,
|
|
1121
|
-
}).catch(() => null);
|
|
1122
|
-
if (listResult && listResult.ok) {
|
|
1123
|
-
const found = (listResult.sessions || []).find(
|
|
1124
|
-
(entry) => normalizeString(entry?.sessionId) === normalizedSessionId,
|
|
1125
|
-
);
|
|
1126
|
-
if (found) {
|
|
1127
|
-
return { ok: true, source: "list_fallback", session: found, status: 200 };
|
|
1128
|
-
}
|
|
1250
|
+
const firstAttempt = await fetchRemoteSessionDetail(endpoint, headers);
|
|
1251
|
+
const detail = firstAttempt.retryable
|
|
1252
|
+
? await fetchRemoteSessionDetail(endpoint, headers)
|
|
1253
|
+
: firstAttempt;
|
|
1254
|
+
if (detail.ok) {
|
|
1255
|
+
return {
|
|
1256
|
+
ok: true,
|
|
1257
|
+
source: "singleton",
|
|
1258
|
+
session: await parseRemoteSessionDetailResponse(detail.response),
|
|
1259
|
+
status: detail.status,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
if (detail.status === 404) {
|
|
1263
|
+
// Pre-#483 fallback: scan the list endpoint once for the same id.
|
|
1264
|
+
const listResult = await listSessionsFromApi({
|
|
1265
|
+
targetPath,
|
|
1266
|
+
includeArchived: false,
|
|
1267
|
+
limit: 50,
|
|
1268
|
+
}).catch(() => null);
|
|
1269
|
+
if (listResult && listResult.ok) {
|
|
1270
|
+
const found = (listResult.sessions || []).find(
|
|
1271
|
+
(entry) => normalizeString(entry?.sessionId) === normalizedSessionId,
|
|
1272
|
+
);
|
|
1273
|
+
if (found) {
|
|
1274
|
+
return { ok: true, source: "list_fallback", session: found, status: 200 };
|
|
1129
1275
|
}
|
|
1130
|
-
return { ok: false, reason: "not_found", status: 404 };
|
|
1131
|
-
}
|
|
1132
|
-
if (response.status === 403) {
|
|
1133
|
-
return { ok: false, reason: "forbidden", status: 403 };
|
|
1134
|
-
}
|
|
1135
|
-
if (response.status >= 500 && response.status < 600) {
|
|
1136
|
-
lastReason = `api_${response.status}`;
|
|
1137
|
-
continue; // retry once on 5xx
|
|
1138
1276
|
}
|
|
1139
|
-
return { ok: false, reason:
|
|
1277
|
+
return { ok: false, reason: "not_found", status: 404 };
|
|
1140
1278
|
}
|
|
1141
|
-
|
|
1279
|
+
if (detail.status === 403) {
|
|
1280
|
+
return { ok: false, reason: "forbidden", status: 403 };
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
ok: false,
|
|
1284
|
+
reason: detail.reason || "unknown",
|
|
1285
|
+
status: detail.status || undefined,
|
|
1286
|
+
};
|
|
1142
1287
|
}
|
|
1143
1288
|
|
|
1144
1289
|
function normalizeRemoteResumeStatus(session = {}) {
|
|
@@ -1953,6 +2098,18 @@ function isSessionControlEvent(event = {}) {
|
|
|
1953
2098
|
return normalizeString(payload.source).toLowerCase() === "session_listen";
|
|
1954
2099
|
}
|
|
1955
2100
|
|
|
2101
|
+
function summarizeSessionTailEvent(event = null) {
|
|
2102
|
+
if (!event || typeof event !== "object") return null;
|
|
2103
|
+
return {
|
|
2104
|
+
type: normalizeString(event.event || event.type) || null,
|
|
2105
|
+
sequenceId: Number(event.sequenceId ?? event.sequence_id) || null,
|
|
2106
|
+
cursor: normalizeString(event.cursor) || null,
|
|
2107
|
+
ts: normalizeString(event.ts || event.timestamp) || null,
|
|
2108
|
+
agentId: normalizeString(event.agent?.id || event.agentId || event.agent_id) || null,
|
|
2109
|
+
control: isSessionControlEvent(event),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
1956
2113
|
function checkpointSequenceRange(checkpoint = {}) {
|
|
1957
2114
|
const start = Number(checkpoint.startSequence || 0);
|
|
1958
2115
|
const end = Number(checkpoint.endSequence || 0);
|
|
@@ -4606,6 +4763,7 @@ export function registerSessionCommand(program) {
|
|
|
4606
4763
|
let hydration = null;
|
|
4607
4764
|
let remoteTail = null;
|
|
4608
4765
|
let remoteActions = null;
|
|
4766
|
+
let remoteTailStats = null;
|
|
4609
4767
|
if (options.remote) {
|
|
4610
4768
|
const authSession = await resolveActiveAuthSession({
|
|
4611
4769
|
cwd: targetPath,
|
|
@@ -4632,6 +4790,23 @@ export function registerSessionCommand(program) {
|
|
|
4632
4790
|
timeoutMs: 15_000,
|
|
4633
4791
|
});
|
|
4634
4792
|
}
|
|
4793
|
+
const remoteTailEvents = remoteTail?.ok && Array.isArray(remoteTail.events) ? remoteTail.events : [];
|
|
4794
|
+
const latestEvent = remoteTailEvents[remoteTailEvents.length - 1] || null;
|
|
4795
|
+
const latestVisibleEvent = [...remoteTailEvents]
|
|
4796
|
+
.reverse()
|
|
4797
|
+
.find((event) => includeControlEvents || !isSessionControlEvent(event)) || null;
|
|
4798
|
+
const controlEventCount = remoteTailEvents.filter((event) => isSessionControlEvent(event)).length;
|
|
4799
|
+
remoteTailStats = {
|
|
4800
|
+
controlEventCount,
|
|
4801
|
+
materialEventCount: Math.max(0, remoteTailEvents.length - controlEventCount),
|
|
4802
|
+
latestEvent: summarizeSessionTailEvent(latestEvent),
|
|
4803
|
+
latestVisibleEvent: summarizeSessionTailEvent(latestVisibleEvent),
|
|
4804
|
+
latestActivityHidden: Boolean(
|
|
4805
|
+
latestEvent &&
|
|
4806
|
+
isSessionControlEvent(latestEvent) &&
|
|
4807
|
+
!includeControlEvents
|
|
4808
|
+
),
|
|
4809
|
+
};
|
|
4635
4810
|
if (!emitJson) {
|
|
4636
4811
|
if (hydration.ok) {
|
|
4637
4812
|
console.log(
|
|
@@ -4660,6 +4835,16 @@ export function registerSessionCommand(program) {
|
|
|
4660
4835
|
),
|
|
4661
4836
|
);
|
|
4662
4837
|
}
|
|
4838
|
+
if (remoteTailStats.latestActivityHidden) {
|
|
4839
|
+
const latest = remoteTailStats.latestEvent;
|
|
4840
|
+
const type = latest?.type || "control event";
|
|
4841
|
+
const seq = latest?.sequenceId ? ` seq=${latest.sequenceId}` : "";
|
|
4842
|
+
console.log(
|
|
4843
|
+
pc.gray(
|
|
4844
|
+
`Latest remote activity is hidden control-plane traffic (${type}${seq}); showing recent material messages. Use --include-control-events to inspect it.`,
|
|
4845
|
+
),
|
|
4846
|
+
);
|
|
4847
|
+
}
|
|
4663
4848
|
}
|
|
4664
4849
|
}
|
|
4665
4850
|
|
|
@@ -4757,6 +4942,11 @@ export function registerSessionCommand(program) {
|
|
|
4757
4942
|
verified: Boolean(remoteTail.ok),
|
|
4758
4943
|
appended: remoteTailAppended,
|
|
4759
4944
|
displayedOnly: remoteTailDisplayedOnly,
|
|
4945
|
+
controlEventCount: remoteTailStats?.controlEventCount || 0,
|
|
4946
|
+
materialEventCount: remoteTailStats?.materialEventCount || 0,
|
|
4947
|
+
latestEvent: remoteTailStats?.latestEvent || null,
|
|
4948
|
+
latestVisibleEvent: remoteTailStats?.latestVisibleEvent || null,
|
|
4949
|
+
latestActivityHidden: Boolean(remoteTailStats?.latestActivityHidden),
|
|
4760
4950
|
}
|
|
4761
4951
|
: null,
|
|
4762
4952
|
actions: remoteActions
|
package/src/legacy-cli.js
CHANGED
|
@@ -1298,6 +1298,8 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1298
1298
|
const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
|
|
1299
1299
|
const modelOverride = getCommandOptionValue(args, "--model") || "";
|
|
1300
1300
|
const providerOverride = getCommandOptionValue(args, "--provider") || "";
|
|
1301
|
+
const notifySession = getCommandOptionValue(args, "--notify-session") || "";
|
|
1302
|
+
const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
|
|
1301
1303
|
const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
|
|
1302
1304
|
const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
|
|
1303
1305
|
const streamEnabled = hasCommandOption(args, "--stream");
|
|
@@ -1388,6 +1390,8 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1388
1390
|
onEvent: streamHandler,
|
|
1389
1391
|
includeOnly: includeOnly.length > 0 ? includeOnly : null,
|
|
1390
1392
|
skipPersonas: skipPersonas.length > 0 ? skipPersonas : null,
|
|
1393
|
+
usageSessionId: notifySession,
|
|
1394
|
+
requireUsageLedger,
|
|
1391
1395
|
});
|
|
1392
1396
|
|
|
1393
1397
|
// Use orchestrator results as the AI layer. aiResult represents ONLY
|
|
@@ -1415,6 +1419,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1415
1419
|
status: p.status,
|
|
1416
1420
|
findings: p.findings || 0,
|
|
1417
1421
|
costUsd: p.costUsd || 0,
|
|
1422
|
+
billing: p.billing || null,
|
|
1418
1423
|
durationMs: p.durationMs || 0,
|
|
1419
1424
|
error: p.error || null,
|
|
1420
1425
|
})),
|
|
@@ -1445,8 +1450,17 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1445
1450
|
outputDir: outputDirArg,
|
|
1446
1451
|
provider: providerOverride || undefined,
|
|
1447
1452
|
model: modelOverride || undefined,
|
|
1453
|
+
sessionId: notifySession,
|
|
1448
1454
|
maxCostUsd,
|
|
1449
1455
|
dryRun: aiDryRun,
|
|
1456
|
+
requireUsageLedger,
|
|
1457
|
+
sourceCommand: "omargate deep",
|
|
1458
|
+
billingAgentId: "omargate-orchestrator",
|
|
1459
|
+
billingAction: "omargate_deep",
|
|
1460
|
+
billingMetadata: {
|
|
1461
|
+
scanMode,
|
|
1462
|
+
},
|
|
1463
|
+
usageFailureLabel: "OmarGate AI",
|
|
1450
1464
|
env: process.env,
|
|
1451
1465
|
});
|
|
1452
1466
|
} catch (aiError) {
|
|
@@ -22,6 +22,8 @@ const SESSION_MCP_CONFIRM_ATTEMPTS = 3;
|
|
|
22
22
|
const SESSION_MCP_CONFIRM_DELAY_MS = 250;
|
|
23
23
|
const SESSION_MCP_CONFIRM_PAGE_LIMIT = 200;
|
|
24
24
|
const SESSION_MCP_CONFIRM_MAX_PAGES = 10;
|
|
25
|
+
const SESSION_MCP_CONFIRM_TOTAL_TIMEOUT_MS = 10_000;
|
|
26
|
+
const SESSION_MCP_CONFIRM_REQUEST_TIMEOUT_MS = 2_000;
|
|
25
27
|
const JSON_RPC_VERSION = "2.0";
|
|
26
28
|
const CONTENT_LENGTH_PREFIX = "content-length:";
|
|
27
29
|
|
|
@@ -48,6 +50,11 @@ function normalizePositiveInteger(value, fallbackValue) {
|
|
|
48
50
|
return Math.floor(normalized);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function confirmationRemainingMs(deadlineMs, nowMs) {
|
|
54
|
+
const now = Number(nowMs()) || Date.now();
|
|
55
|
+
return Math.max(0, Math.floor(deadlineMs - now));
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
function normalizeLimit(value) {
|
|
52
59
|
return Math.max(1, Math.min(MAX_TOOL_LIMIT, normalizePositiveInteger(value, DEFAULT_TOOL_LIMIT)));
|
|
53
60
|
}
|
|
@@ -199,32 +206,111 @@ async function confirmSessionEventVisible(
|
|
|
199
206
|
targetPath,
|
|
200
207
|
anchorCursor = null,
|
|
201
208
|
pollSessionEventsFn = pollSessionEvents,
|
|
209
|
+
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
202
210
|
sleepFn = sleep,
|
|
211
|
+
nowMs = Date.now,
|
|
203
212
|
attempts = SESSION_MCP_CONFIRM_ATTEMPTS,
|
|
204
213
|
delayMs = SESSION_MCP_CONFIRM_DELAY_MS,
|
|
205
214
|
pageLimit = SESSION_MCP_CONFIRM_PAGE_LIMIT,
|
|
206
215
|
maxPages = SESSION_MCP_CONFIRM_MAX_PAGES,
|
|
216
|
+
totalTimeoutMs = SESSION_MCP_CONFIRM_TOTAL_TIMEOUT_MS,
|
|
217
|
+
requestTimeoutMs = SESSION_MCP_CONFIRM_REQUEST_TIMEOUT_MS,
|
|
207
218
|
} = {},
|
|
208
219
|
) {
|
|
209
220
|
let lastReason = "not_visible";
|
|
210
221
|
let checked = 0;
|
|
211
222
|
let pages = 0;
|
|
223
|
+
let tailChecks = 0;
|
|
212
224
|
const normalizedAnchorCursor = normalizeString(anchorCursor) || null;
|
|
213
225
|
const normalizedAttempts = normalizePositiveInteger(attempts, SESSION_MCP_CONFIRM_ATTEMPTS);
|
|
214
226
|
const normalizedDelayMs = normalizePositiveInteger(delayMs, SESSION_MCP_CONFIRM_DELAY_MS);
|
|
215
227
|
const normalizedPageLimit = normalizeLimit(pageLimit);
|
|
216
228
|
const normalizedMaxPages = normalizePositiveInteger(maxPages, SESSION_MCP_CONFIRM_MAX_PAGES);
|
|
229
|
+
const normalizedTotalTimeoutMs = normalizePositiveInteger(
|
|
230
|
+
totalTimeoutMs,
|
|
231
|
+
SESSION_MCP_CONFIRM_TOTAL_TIMEOUT_MS,
|
|
232
|
+
);
|
|
233
|
+
const normalizedRequestTimeoutMs = normalizePositiveInteger(
|
|
234
|
+
requestTimeoutMs,
|
|
235
|
+
SESSION_MCP_CONFIRM_REQUEST_TIMEOUT_MS,
|
|
236
|
+
);
|
|
237
|
+
const deadlineMs = (Number(nowMs()) || Date.now()) + normalizedTotalTimeoutMs;
|
|
238
|
+
const nextRequestTimeoutMs = () => {
|
|
239
|
+
const remaining = confirmationRemainingMs(deadlineMs, nowMs);
|
|
240
|
+
if (remaining <= 0) return 0;
|
|
241
|
+
return Math.max(1, Math.min(normalizedRequestTimeoutMs, remaining));
|
|
242
|
+
};
|
|
217
243
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
244
|
+
const maybeConfirmFromLatestTail = async () => {
|
|
245
|
+
const timeoutMs = nextRequestTimeoutMs();
|
|
246
|
+
if (timeoutMs <= 0) {
|
|
247
|
+
lastReason = "confirmation_timeout";
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
tailChecks += 1;
|
|
251
|
+
let result = null;
|
|
252
|
+
try {
|
|
253
|
+
result = await pollSessionEventsBeforeFn(sessionId, {
|
|
223
254
|
targetPath,
|
|
224
|
-
since: pageCursor,
|
|
225
255
|
limit: normalizedPageLimit,
|
|
256
|
+
timeoutMs,
|
|
226
257
|
forceCircuitProbe: true,
|
|
227
258
|
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
lastReason = normalizeString(error?.code || error?.message) || "confirmation_tail_poll_failed";
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
if (!result?.ok) {
|
|
264
|
+
lastReason = normalizeString(result?.reason) || "confirmation_tail_poll_failed";
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
268
|
+
checked += events.length;
|
|
269
|
+
const confirmedEvent = events.find((candidate) => sessionEventMatchesClientMessageId(candidate, clientMessageId));
|
|
270
|
+
if (!confirmedEvent) {
|
|
271
|
+
lastReason = "not_visible";
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
confirmed: true,
|
|
276
|
+
reason: "",
|
|
277
|
+
checked,
|
|
278
|
+
pages,
|
|
279
|
+
tailChecks,
|
|
280
|
+
source: "latest_tail",
|
|
281
|
+
anchorCursor: normalizedAnchorCursor,
|
|
282
|
+
event: confirmedEvent,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (let attempt = 1; attempt <= normalizedAttempts; attempt += 1) {
|
|
287
|
+
let pageCursor = normalizedAnchorCursor;
|
|
288
|
+
for (let page = 1; page <= normalizedMaxPages; page += 1) {
|
|
289
|
+
const timeoutMs = nextRequestTimeoutMs();
|
|
290
|
+
if (timeoutMs <= 0) {
|
|
291
|
+
return {
|
|
292
|
+
confirmed: false,
|
|
293
|
+
reason: "confirmation_timeout",
|
|
294
|
+
checked,
|
|
295
|
+
pages,
|
|
296
|
+
tailChecks,
|
|
297
|
+
anchorCursor: normalizedAnchorCursor,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
pages += 1;
|
|
301
|
+
let result = null;
|
|
302
|
+
try {
|
|
303
|
+
result = await pollSessionEventsFn(sessionId, {
|
|
304
|
+
targetPath,
|
|
305
|
+
since: pageCursor,
|
|
306
|
+
limit: normalizedPageLimit,
|
|
307
|
+
timeoutMs,
|
|
308
|
+
forceCircuitProbe: true,
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
lastReason = normalizeString(error?.code || error?.message) || "confirmation_poll_failed";
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
228
314
|
if (!result?.ok) {
|
|
229
315
|
lastReason = normalizeString(result?.reason) || "confirmation_poll_failed";
|
|
230
316
|
break;
|
|
@@ -238,6 +324,8 @@ async function confirmSessionEventVisible(
|
|
|
238
324
|
reason: "",
|
|
239
325
|
checked,
|
|
240
326
|
pages,
|
|
327
|
+
tailChecks,
|
|
328
|
+
source: "forward_cursor",
|
|
241
329
|
anchorCursor: normalizedAnchorCursor,
|
|
242
330
|
event: confirmedEvent,
|
|
243
331
|
};
|
|
@@ -249,8 +337,17 @@ async function confirmSessionEventVisible(
|
|
|
249
337
|
}
|
|
250
338
|
pageCursor = nextCursor;
|
|
251
339
|
}
|
|
340
|
+
const tailConfirmation = await maybeConfirmFromLatestTail();
|
|
341
|
+
if (tailConfirmation) {
|
|
342
|
+
return tailConfirmation;
|
|
343
|
+
}
|
|
252
344
|
if (attempt < normalizedAttempts) {
|
|
253
|
-
|
|
345
|
+
const remaining = confirmationRemainingMs(deadlineMs, nowMs);
|
|
346
|
+
if (remaining <= 0) {
|
|
347
|
+
lastReason = "confirmation_timeout";
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
await sleepFn(Math.min(normalizedDelayMs, remaining));
|
|
254
351
|
}
|
|
255
352
|
}
|
|
256
353
|
|
|
@@ -259,6 +356,7 @@ async function confirmSessionEventVisible(
|
|
|
259
356
|
reason: lastReason,
|
|
260
357
|
checked,
|
|
261
358
|
pages,
|
|
359
|
+
tailChecks,
|
|
262
360
|
anchorCursor: normalizedAnchorCursor,
|
|
263
361
|
};
|
|
264
362
|
}
|
|
@@ -441,6 +539,7 @@ async function persistSessionEvent({
|
|
|
441
539
|
targetPath,
|
|
442
540
|
anchorCursor: remoteConfirmationAnchor.cursor,
|
|
443
541
|
pollSessionEventsFn,
|
|
542
|
+
pollSessionEventsBeforeFn,
|
|
444
543
|
sleepFn,
|
|
445
544
|
attempts: confirmationAttempts,
|
|
446
545
|
delayMs: confirmationDelayMs,
|
package/src/review/ai-review.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
resolveProvider,
|
|
9
9
|
} from "../ai/client.js";
|
|
10
10
|
import { computeProviderCost } from "../billing/price-book.js";
|
|
11
|
-
import { buildBillingRunId, buildCallIdempotencyKey } from "../billing/ledger-entry.js";
|
|
11
|
+
import { buildBillingRunId, buildCallIdempotencyKey, sanitizeBillingMetadata } from "../billing/ledger-entry.js";
|
|
12
12
|
import { recordSessionUsage } from "../billing/session-usage.js";
|
|
13
13
|
import { loadConfig } from "../config/service.js";
|
|
14
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
@@ -488,6 +488,12 @@ export async function runAiReviewLayer({
|
|
|
488
488
|
dryRun = false,
|
|
489
489
|
requireUsageLedger = false,
|
|
490
490
|
usageRecorder = recordSessionUsage,
|
|
491
|
+
sourceCommand = "review",
|
|
492
|
+
billingAgentId = "audit-orchestrator",
|
|
493
|
+
billingAction = "audit_run",
|
|
494
|
+
billingTier = "internal",
|
|
495
|
+
billingMetadata = {},
|
|
496
|
+
usageFailureLabel = "Review AI",
|
|
491
497
|
env = process.env,
|
|
492
498
|
} = {}) {
|
|
493
499
|
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
@@ -499,6 +505,12 @@ export async function runAiReviewLayer({
|
|
|
499
505
|
);
|
|
500
506
|
const normalizedRunId = normalizeString(runId) || "review-ai";
|
|
501
507
|
const normalizedRequireUsageLedger = Boolean(requireUsageLedger);
|
|
508
|
+
const normalizedSourceCommand = normalizeString(sourceCommand) || "review";
|
|
509
|
+
const normalizedBillingAgentId = normalizeString(billingAgentId) || "audit-orchestrator";
|
|
510
|
+
const normalizedBillingAction = normalizeString(billingAction) || "audit_run";
|
|
511
|
+
const normalizedBillingTier = normalizeString(billingTier) || "internal";
|
|
512
|
+
const normalizedBillingMetadata = sanitizeBillingMetadata(billingMetadata);
|
|
513
|
+
const normalizedUsageFailureLabel = normalizeString(usageFailureLabel) || "Review AI";
|
|
502
514
|
|
|
503
515
|
const config = await loadConfig({ cwd: normalizedTargetPath, env });
|
|
504
516
|
let resolvedProvider = resolveProvider({
|
|
@@ -641,7 +653,7 @@ export async function runAiReviewLayer({
|
|
|
641
653
|
toolCalls: 1,
|
|
642
654
|
},
|
|
643
655
|
metadata: {
|
|
644
|
-
sourceCommand:
|
|
656
|
+
sourceCommand: normalizedSourceCommand,
|
|
645
657
|
layer: "ai_reasoning",
|
|
646
658
|
provider: resolvedProvider,
|
|
647
659
|
model: resolvedModel,
|
|
@@ -657,28 +669,31 @@ export async function runAiReviewLayer({
|
|
|
657
669
|
sessionId: normalizedSessionId,
|
|
658
670
|
invocationTimestamp: startedAtIso,
|
|
659
671
|
configHash: stableConfigHash({
|
|
660
|
-
sourceCommand:
|
|
672
|
+
sourceCommand: normalizedSourceCommand,
|
|
661
673
|
layer: "ai_reasoning",
|
|
662
674
|
provider: resolvedProvider,
|
|
663
675
|
model: resolvedModel,
|
|
664
676
|
mode: normalizedMode,
|
|
665
677
|
runId: normalizedRunId,
|
|
666
678
|
dryRun: Boolean(dryRun),
|
|
679
|
+
billingAgentId: normalizedBillingAgentId,
|
|
680
|
+
billingAction: normalizedBillingAction,
|
|
667
681
|
}),
|
|
668
682
|
});
|
|
669
683
|
sessionUsageLedger = await usageRecorder(
|
|
670
684
|
normalizedSessionId,
|
|
671
685
|
{
|
|
672
|
-
agentId:
|
|
673
|
-
action:
|
|
686
|
+
agentId: normalizedBillingAgentId,
|
|
687
|
+
action: normalizedBillingAction,
|
|
674
688
|
model: resolvedModel,
|
|
675
689
|
inputTokens,
|
|
676
690
|
outputTokens,
|
|
677
691
|
idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
|
|
678
|
-
billingTier:
|
|
692
|
+
billingTier: normalizedBillingTier,
|
|
679
693
|
createdAt: startedAtIso,
|
|
680
694
|
metadata: {
|
|
681
|
-
|
|
695
|
+
...normalizedBillingMetadata,
|
|
696
|
+
sourceCommand: normalizedSourceCommand,
|
|
682
697
|
layer: "ai_reasoning",
|
|
683
698
|
provider: resolvedProvider,
|
|
684
699
|
mode: normalizedMode,
|
|
@@ -698,7 +713,7 @@ export async function runAiReviewLayer({
|
|
|
698
713
|
};
|
|
699
714
|
if (normalizedRequireUsageLedger) {
|
|
700
715
|
throw new Error(
|
|
701
|
-
|
|
716
|
+
`${normalizedUsageFailureLabel} usage ledger recording failed: ${sessionUsageLedger.reason || "unknown"}.`
|
|
702
717
|
);
|
|
703
718
|
}
|
|
704
719
|
}
|
|
@@ -729,7 +744,7 @@ export async function runAiReviewLayer({
|
|
|
729
744
|
reasonCodes: budget.reasons.map((reason) => reason.code),
|
|
730
745
|
},
|
|
731
746
|
metadata: {
|
|
732
|
-
sourceCommand:
|
|
747
|
+
sourceCommand: normalizedSourceCommand,
|
|
733
748
|
layer: "ai_reasoning",
|
|
734
749
|
provider: resolvedProvider,
|
|
735
750
|
model: resolvedModel,
|
|
@@ -423,6 +423,10 @@ function omargateConfidenceFloorForPersona(personaId) {
|
|
|
423
423
|
return OMARGATE_CONFIDENCE_FLOORS[personaId] || OMARGATE_DEFAULT_CONFIDENCE_FLOOR;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
function isUsageLedgerFailure(message = "") {
|
|
427
|
+
return /usage ledger/i.test(String(message || ""));
|
|
428
|
+
}
|
|
429
|
+
|
|
426
430
|
async function runOmarPersonaSwarm({
|
|
427
431
|
personaId,
|
|
428
432
|
identity,
|
|
@@ -436,6 +440,9 @@ async function runOmarPersonaSwarm({
|
|
|
436
440
|
perPersonaCost,
|
|
437
441
|
dryRun,
|
|
438
442
|
onEvent,
|
|
443
|
+
usageSessionId = "",
|
|
444
|
+
requireUsageLedger = false,
|
|
445
|
+
usageRecorder = undefined,
|
|
439
446
|
} = {}) {
|
|
440
447
|
const scope = buildPersonaFileScope({ deterministic });
|
|
441
448
|
const decision = decideSwarm({ scope });
|
|
@@ -537,10 +544,26 @@ async function runOmarPersonaSwarm({
|
|
|
537
544
|
outputDir,
|
|
538
545
|
provider: provider || undefined,
|
|
539
546
|
model: model || undefined,
|
|
540
|
-
sessionId: `${subagentRunId}-ai`,
|
|
547
|
+
sessionId: usageSessionId || `${subagentRunId}-ai`,
|
|
541
548
|
maxCostUsd: budget.maxCostUsd,
|
|
542
549
|
systemPrompt,
|
|
543
550
|
dryRun,
|
|
551
|
+
requireUsageLedger,
|
|
552
|
+
usageRecorder,
|
|
553
|
+
sourceCommand: "omargate deep",
|
|
554
|
+
billingAgentId: `omargate-${personaId}-subagent-${subagentIndex}`,
|
|
555
|
+
billingAction: "omargate_deep",
|
|
556
|
+
billingMetadata: {
|
|
557
|
+
personaId,
|
|
558
|
+
subagentIndex,
|
|
559
|
+
partitionCount: partitions.length,
|
|
560
|
+
parentRunId: runId,
|
|
561
|
+
swarmRunId,
|
|
562
|
+
subagentRunId,
|
|
563
|
+
scanMode: mode,
|
|
564
|
+
fileCount: scopedFiles.length,
|
|
565
|
+
},
|
|
566
|
+
usageFailureLabel: "OmarGate AI",
|
|
544
567
|
env: process.env,
|
|
545
568
|
});
|
|
546
569
|
|
|
@@ -610,6 +633,7 @@ async function runOmarPersonaSwarm({
|
|
|
610
633
|
summary: result?.summary || summarizeFindings(findings),
|
|
611
634
|
costUsd: result?.usage?.costUsd || 0,
|
|
612
635
|
model: result?.model || model || null,
|
|
636
|
+
billing: result?.billing || null,
|
|
613
637
|
artifacts: result?.artifacts || null,
|
|
614
638
|
durationMs: Date.now() - subagentStart,
|
|
615
639
|
};
|
|
@@ -671,6 +695,41 @@ async function runOmarPersonaSwarm({
|
|
|
671
695
|
const summary = summarizeFindings(findings);
|
|
672
696
|
const okCount = settledSubagents.filter((result) => result.status === "ok").length;
|
|
673
697
|
const errorCount = settledSubagents.filter((result) => result.status === "error").length;
|
|
698
|
+
const usageLedgerErrors = settledSubagents.filter((result) =>
|
|
699
|
+
result.status === "error" && isUsageLedgerFailure(result.error)
|
|
700
|
+
);
|
|
701
|
+
if (requireUsageLedger && usageLedgerErrors.length > 0) {
|
|
702
|
+
return {
|
|
703
|
+
personaId,
|
|
704
|
+
status: "error",
|
|
705
|
+
findings: [],
|
|
706
|
+
summary: { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false },
|
|
707
|
+
costUsd: totalCostUsd,
|
|
708
|
+
model: model || null,
|
|
709
|
+
durationMs: Date.now() - startedAt,
|
|
710
|
+
error: `OmarGate required usage ledger failed for ${usageLedgerErrors.length}/${settledSubagents.length} subagent(s).`,
|
|
711
|
+
swarm: {
|
|
712
|
+
runId: swarmRunId,
|
|
713
|
+
decision,
|
|
714
|
+
subagentCount: settledSubagents.length,
|
|
715
|
+
ok: okCount,
|
|
716
|
+
error: errorCount,
|
|
717
|
+
partitionSizes: partitions.map((files) => files.length),
|
|
718
|
+
blackboardEntries: blackboard.length,
|
|
719
|
+
subagents: settledSubagents.map((result) => ({
|
|
720
|
+
id: result.agentId,
|
|
721
|
+
index: result.subagentIndex,
|
|
722
|
+
status: result.status,
|
|
723
|
+
files: result.files,
|
|
724
|
+
findings: (result.findings || []).length,
|
|
725
|
+
costUsd: result.costUsd || 0,
|
|
726
|
+
durationMs: result.durationMs || 0,
|
|
727
|
+
error: result.error || null,
|
|
728
|
+
artifacts: result.artifacts || null,
|
|
729
|
+
})),
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
674
733
|
|
|
675
734
|
if (onEvent) {
|
|
676
735
|
onEvent(createAgentEvent({
|
|
@@ -723,6 +782,7 @@ async function runOmarPersonaSwarm({
|
|
|
723
782
|
files: result.files,
|
|
724
783
|
findings: (result.findings || []).length,
|
|
725
784
|
costUsd: result.costUsd || 0,
|
|
785
|
+
billing: result.billing || null,
|
|
726
786
|
durationMs: result.durationMs || 0,
|
|
727
787
|
error: result.error || null,
|
|
728
788
|
artifacts: result.artifacts || null,
|
|
@@ -747,6 +807,8 @@ async function runOmarPersonaSwarm({
|
|
|
747
807
|
* @param {Function} [options.onEvent] - Event callback for streaming
|
|
748
808
|
* @param {string[] | null} [options.includeOnly] - Only run these persona IDs (filters scan-mode roster).
|
|
749
809
|
* @param {string[] | null} [options.skipPersonas] - Skip these persona IDs (filters scan-mode roster).
|
|
810
|
+
* @param {boolean} [options.requireUsageLedger] - Fail if Omar Deep AI calls cannot write billing-grade session_usage.
|
|
811
|
+
* @param {string} [options.usageSessionId] - Optional shared Senti session id for billing-grade usage entries.
|
|
750
812
|
* @returns {Promise<object>} Orchestrated results
|
|
751
813
|
*/
|
|
752
814
|
export async function runOmarGateOrchestrator({
|
|
@@ -762,6 +824,9 @@ export async function runOmarGateOrchestrator({
|
|
|
762
824
|
onEvent = null,
|
|
763
825
|
includeOnly = null,
|
|
764
826
|
skipPersonas = null,
|
|
827
|
+
requireUsageLedger = false,
|
|
828
|
+
usageSessionId = "",
|
|
829
|
+
usageRecorder = undefined,
|
|
765
830
|
} = {}) {
|
|
766
831
|
const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
767
832
|
const startTime = Date.now();
|
|
@@ -931,6 +996,9 @@ export async function runOmarGateOrchestrator({
|
|
|
931
996
|
perPersonaCost,
|
|
932
997
|
dryRun,
|
|
933
998
|
onEvent,
|
|
999
|
+
requireUsageLedger,
|
|
1000
|
+
usageSessionId,
|
|
1001
|
+
usageRecorder,
|
|
934
1002
|
});
|
|
935
1003
|
|
|
936
1004
|
if (swarmResult) {
|
|
@@ -999,9 +1067,21 @@ export async function runOmarGateOrchestrator({
|
|
|
999
1067
|
outputDir,
|
|
1000
1068
|
provider: provider || undefined,
|
|
1001
1069
|
model: model || undefined,
|
|
1070
|
+
sessionId: usageSessionId || `${runId}-${personaId}-ai`,
|
|
1002
1071
|
maxCostUsd: perPersonaCost,
|
|
1003
1072
|
systemPrompt,
|
|
1004
1073
|
dryRun,
|
|
1074
|
+
requireUsageLedger,
|
|
1075
|
+
usageRecorder,
|
|
1076
|
+
sourceCommand: "omargate deep",
|
|
1077
|
+
billingAgentId: `omargate-${personaId}`,
|
|
1078
|
+
billingAction: "omargate_deep",
|
|
1079
|
+
billingMetadata: {
|
|
1080
|
+
personaId,
|
|
1081
|
+
parentRunId: runId,
|
|
1082
|
+
scanMode: mode,
|
|
1083
|
+
},
|
|
1084
|
+
usageFailureLabel: "OmarGate AI",
|
|
1005
1085
|
env: process.env,
|
|
1006
1086
|
});
|
|
1007
1087
|
|
|
@@ -1062,6 +1142,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1062
1142
|
summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
1063
1143
|
costUsd: personaCost,
|
|
1064
1144
|
model: result?.model || model || null,
|
|
1145
|
+
billing: result?.billing || null,
|
|
1065
1146
|
artifacts: result?.artifacts || null,
|
|
1066
1147
|
durationMs: Date.now() - personaStart,
|
|
1067
1148
|
};
|
|
@@ -1106,6 +1187,18 @@ export async function runOmarGateOrchestrator({
|
|
|
1106
1187
|
durationMs: 0,
|
|
1107
1188
|
})
|
|
1108
1189
|
);
|
|
1190
|
+
const usageLedgerFailures = settled.filter((r) =>
|
|
1191
|
+
r.status === "error" && isUsageLedgerFailure(r.error)
|
|
1192
|
+
);
|
|
1193
|
+
if (requireUsageLedger && usageLedgerFailures.length > 0) {
|
|
1194
|
+
const examples = usageLedgerFailures
|
|
1195
|
+
.slice(0, 3)
|
|
1196
|
+
.map((r) => `${r.personaId}: ${r.error}`)
|
|
1197
|
+
.join("; ");
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`OmarGate required usage ledger recording failed for ${usageLedgerFailures.length}/${settled.length} persona(s).${examples ? ` ${examples}` : ""}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1109
1202
|
|
|
1110
1203
|
// Reconcile AI findings with deterministic findings — canonical single list.
|
|
1111
1204
|
// Confidence boost when multiple layers agree; deterministic findings get
|
|
@@ -1183,6 +1276,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1183
1276
|
costUsd: r.costUsd,
|
|
1184
1277
|
durationMs: r.durationMs,
|
|
1185
1278
|
model: r.model || null,
|
|
1279
|
+
billing: r.billing || null,
|
|
1186
1280
|
error: r.error || null,
|
|
1187
1281
|
swarm: r.swarm || null,
|
|
1188
1282
|
artifacts: r.artifacts || null,
|