sentinelayer-cli 0.30.6 → 0.30.8
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
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -5867,10 +5867,11 @@ export function registerSessionCommand(program) {
|
|
|
5867
5867
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
5868
5868
|
: [];
|
|
5869
5869
|
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
5870
|
+
const dedupedEvents = dedupeSessionEvents(events);
|
|
5870
5871
|
const transcriptBaseEvents = includeControlEvents
|
|
5871
|
-
?
|
|
5872
|
-
: filterSessionMaterialEvents(
|
|
5873
|
-
const hiddenControlEventCount =
|
|
5872
|
+
? dedupedEvents
|
|
5873
|
+
: filterSessionMaterialEvents(dedupedEvents);
|
|
5874
|
+
const hiddenControlEventCount = dedupedEvents.length - transcriptBaseEvents.length;
|
|
5874
5875
|
const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
5875
5876
|
const stats = computeTranscriptStats({
|
|
5876
5877
|
sessionMeta: sessionPayload,
|
|
@@ -6021,10 +6022,11 @@ export function registerSessionCommand(program) {
|
|
|
6021
6022
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
6022
6023
|
: [];
|
|
6023
6024
|
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
6025
|
+
const dedupedEvents = dedupeSessionEvents(events);
|
|
6024
6026
|
const transcriptBaseEvents = includeControlEvents
|
|
6025
|
-
?
|
|
6026
|
-
: filterSessionMaterialEvents(
|
|
6027
|
-
const hiddenControlEventCount =
|
|
6027
|
+
? dedupedEvents
|
|
6028
|
+
: filterSessionMaterialEvents(dedupedEvents);
|
|
6029
|
+
const hiddenControlEventCount = dedupedEvents.length - transcriptBaseEvents.length;
|
|
6028
6030
|
const transcriptEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
6029
6031
|
|
|
6030
6032
|
// Pull GitHub/Google avatar + display name from the active auth
|
|
@@ -49,7 +49,12 @@ const INVESTOR_DD_PERSONAS = Object.freeze([...FULL_DEPTH_PERSONAS]);
|
|
|
49
49
|
* Walk the target repo and return a list of relative POSIX file paths.
|
|
50
50
|
* Skips common noise directories.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
function isPathInsideOrSame(candidate, root) {
|
|
53
|
+
const rel = path.relative(root, candidate);
|
|
54
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function walkRepoFiles(rootPath, { excludeRoots = [] } = {}) {
|
|
53
58
|
const SKIP_DIRS = new Set([
|
|
54
59
|
"node_modules",
|
|
55
60
|
".git",
|
|
@@ -84,7 +89,11 @@ async function walkRepoFiles(rootPath) {
|
|
|
84
89
|
]);
|
|
85
90
|
|
|
86
91
|
const results = [];
|
|
92
|
+
const excludedRoots = excludeRoots.map((item) => path.resolve(item));
|
|
87
93
|
async function walk(absDir, relDir) {
|
|
94
|
+
if (excludedRoots.some((excludedRoot) => isPathInsideOrSame(absDir, excludedRoot))) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
88
97
|
let entries;
|
|
89
98
|
try {
|
|
90
99
|
entries = await fsp.readdir(absDir, { withFileTypes: true });
|
|
@@ -99,6 +108,7 @@ async function walkRepoFiles(rootPath) {
|
|
|
99
108
|
const absPath = path.join(absDir, entry.name);
|
|
100
109
|
if (entry.isDirectory()) {
|
|
101
110
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
111
|
+
if (excludedRoots.some((excludedRoot) => isPathInsideOrSame(absPath, excludedRoot))) continue;
|
|
102
112
|
await walk(absPath, relPath);
|
|
103
113
|
} else if (entry.isFile()) {
|
|
104
114
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -111,6 +121,45 @@ async function walkRepoFiles(rootPath) {
|
|
|
111
121
|
return results;
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
function countSourceLoc(text) {
|
|
125
|
+
return String(text || "")
|
|
126
|
+
.split(/\r\n|\n|\r/)
|
|
127
|
+
.filter((line) => line.trim().length > 0).length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function collectFileMetrics(rootPath, files) {
|
|
131
|
+
const metrics = {};
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const rel = String(file || "").trim();
|
|
134
|
+
if (!rel) continue;
|
|
135
|
+
const abs = path.resolve(rootPath, rel);
|
|
136
|
+
try {
|
|
137
|
+
const stat = await fsp.stat(abs);
|
|
138
|
+
if (!stat.isFile()) continue;
|
|
139
|
+
let loc = 0;
|
|
140
|
+
let truncated = false;
|
|
141
|
+
if (stat.size <= 2_000_000) {
|
|
142
|
+
const text = await fsp.readFile(abs, "utf-8");
|
|
143
|
+
loc = countSourceLoc(text);
|
|
144
|
+
} else {
|
|
145
|
+
truncated = true;
|
|
146
|
+
}
|
|
147
|
+
metrics[rel] = {
|
|
148
|
+
bytes: stat.size,
|
|
149
|
+
loc,
|
|
150
|
+
truncated,
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
metrics[rel] = {
|
|
154
|
+
bytes: 0,
|
|
155
|
+
loc: 0,
|
|
156
|
+
missing: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return metrics;
|
|
161
|
+
}
|
|
162
|
+
|
|
114
163
|
async function writeJson(filePath, obj) {
|
|
115
164
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
116
165
|
await fsp.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
@@ -317,11 +366,18 @@ export async function runInvestorDd({
|
|
|
317
366
|
const startTime = Date.now();
|
|
318
367
|
emit({ type: "investor_dd_start", rootPath, personas });
|
|
319
368
|
|
|
320
|
-
const files = await walkRepoFiles(rootPath);
|
|
369
|
+
const files = await walkRepoFiles(rootPath, { excludeRoots: [runRoot] });
|
|
321
370
|
emit({ type: "investor_dd_files_discovered", totalFiles: files.length });
|
|
322
371
|
|
|
323
372
|
const routing = routeFilesToPersonas({ files, personas });
|
|
324
373
|
const routingSummary = summarizeRouting(routing);
|
|
374
|
+
const fileMetrics = await collectFileMetrics(rootPath, files);
|
|
375
|
+
await writeJson(path.join(artifactBase, "file-metrics.json"), {
|
|
376
|
+
runId,
|
|
377
|
+
rootPath,
|
|
378
|
+
totalFiles: files.length,
|
|
379
|
+
metrics: fileMetrics,
|
|
380
|
+
});
|
|
325
381
|
await writeJson(path.join(artifactBase, "plan.json"), {
|
|
326
382
|
runId,
|
|
327
383
|
rootPath,
|
|
@@ -520,6 +576,7 @@ export async function runInvestorDd({
|
|
|
520
576
|
notification,
|
|
521
577
|
artifactFiles: artifactFilesBeforeManifest,
|
|
522
578
|
budgetState,
|
|
579
|
+
fileMetrics,
|
|
523
580
|
});
|
|
524
581
|
summary.ddProgress = {
|
|
525
582
|
version: ddProgress.version,
|
|
@@ -531,6 +588,7 @@ export async function runInvestorDd({
|
|
|
531
588
|
blockingGapCount: ddProgress.summary.blockingGapCount,
|
|
532
589
|
artifact: "progress.json",
|
|
533
590
|
};
|
|
591
|
+
summary.usageTelemetry = ddProgress.usageTelemetry;
|
|
534
592
|
runResult.progress = ddProgress;
|
|
535
593
|
await writeJson(path.join(artifactBase, "progress.json"), ddProgress);
|
|
536
594
|
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
@@ -180,6 +180,7 @@ export async function runPersonaAcrossFiles({
|
|
|
180
180
|
onEvent = () => {},
|
|
181
181
|
} = {}) {
|
|
182
182
|
if (!Array.isArray(files)) throw new TypeError("runPersonaAcrossFiles requires files array");
|
|
183
|
+
const startedAtMs = Date.now();
|
|
183
184
|
const safeBudget = budget || createBudgetState();
|
|
184
185
|
const perFile = [];
|
|
185
186
|
const allFindings = [];
|
|
@@ -228,6 +229,7 @@ export async function runPersonaAcrossFiles({
|
|
|
228
229
|
visited,
|
|
229
230
|
skipped,
|
|
230
231
|
terminationReason,
|
|
232
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
231
233
|
};
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -268,6 +270,7 @@ export async function runPersonaAcrossFiles({
|
|
|
268
270
|
visited,
|
|
269
271
|
skipped,
|
|
270
272
|
terminationReason,
|
|
273
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -29,13 +29,26 @@ function compactBudgetState(budgetState) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function nonNegativeNumber(value, fallback = 0) {
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function roundedMoney(value) {
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (!Number.isFinite(parsed)) return null;
|
|
41
|
+
return Math.round(parsed * 1_000_000) / 1_000_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function usageLedgerKey(entry) {
|
|
33
45
|
return [
|
|
34
46
|
entry?.ledgerEntry?.ledgerEntryId,
|
|
35
|
-
entry?.action,
|
|
47
|
+
entry?.action || entry?.ledgerEntry?.action,
|
|
48
|
+
entry?.agentId || entry?.ledgerEntry?.agentId,
|
|
36
49
|
entry?.ledgerEntry?.idempotencyKey,
|
|
37
|
-
entry?.inputTokens,
|
|
38
|
-
entry?.outputTokens,
|
|
50
|
+
entry?.inputTokens ?? entry?.ledgerEntry?.inputTokens,
|
|
51
|
+
entry?.outputTokens ?? entry?.ledgerEntry?.outputTokens,
|
|
39
52
|
].map((value) => String(value || "")).join(":");
|
|
40
53
|
}
|
|
41
54
|
|
|
@@ -56,6 +69,205 @@ function collectSessionUsageLedgerEntries({ budgetState = null, devTestBotPhase
|
|
|
56
69
|
return entries;
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
function ledgerValue(entry, key, fallback = 0) {
|
|
73
|
+
if (entry && Object.prototype.hasOwnProperty.call(entry, key)) {
|
|
74
|
+
return nonNegativeNumber(entry[key], fallback);
|
|
75
|
+
}
|
|
76
|
+
if (entry?.ledgerEntry && Object.prototype.hasOwnProperty.call(entry.ledgerEntry, key)) {
|
|
77
|
+
return nonNegativeNumber(entry.ledgerEntry[key], fallback);
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ledgerString(entry, key) {
|
|
83
|
+
return String(entry?.[key] || entry?.ledgerEntry?.[key] || "").trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function personaFromAgentId(agentId) {
|
|
87
|
+
const normalized = String(agentId || "").trim();
|
|
88
|
+
if (!normalized.startsWith("investor-dd-")) return "";
|
|
89
|
+
return normalized.slice("investor-dd-".length);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sumFileMetrics(files, fileMetrics = {}) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
let filesWithMetrics = 0;
|
|
95
|
+
let locScanned = 0;
|
|
96
|
+
let bytesScanned = 0;
|
|
97
|
+
let truncatedFiles = 0;
|
|
98
|
+
let missingFiles = 0;
|
|
99
|
+
for (const file of files || []) {
|
|
100
|
+
const rel = String(file || "").trim();
|
|
101
|
+
if (!rel || seen.has(rel)) continue;
|
|
102
|
+
seen.add(rel);
|
|
103
|
+
const metrics = fileMetrics[rel];
|
|
104
|
+
if (!metrics) continue;
|
|
105
|
+
filesWithMetrics += 1;
|
|
106
|
+
locScanned += nonNegativeNumber(metrics.loc, 0);
|
|
107
|
+
bytesScanned += nonNegativeNumber(metrics.bytes, 0);
|
|
108
|
+
if (metrics.truncated) truncatedFiles += 1;
|
|
109
|
+
if (metrics.missing) missingFiles += 1;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
filesWithMetrics,
|
|
113
|
+
locScanned,
|
|
114
|
+
bytesScanned,
|
|
115
|
+
truncatedFiles,
|
|
116
|
+
missingFiles,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function countToolInvocations(record = {}) {
|
|
121
|
+
return (Array.isArray(record.perFile) ? record.perFile : []).reduce(
|
|
122
|
+
(count, item) => count + (Array.isArray(item.toolInvocations) ? item.toolInvocations.length : 0),
|
|
123
|
+
0,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createUsageRecord({ personaId = "", agentId = "" } = {}) {
|
|
128
|
+
const id = String(personaId || "").trim();
|
|
129
|
+
const agent = String(agentId || (id ? `investor-dd-${id}` : "")).trim();
|
|
130
|
+
return {
|
|
131
|
+
personaId: id || null,
|
|
132
|
+
agentId: agent,
|
|
133
|
+
routedFiles: 0,
|
|
134
|
+
visitedFiles: 0,
|
|
135
|
+
skippedFiles: 0,
|
|
136
|
+
filesWithMetrics: 0,
|
|
137
|
+
locScanned: 0,
|
|
138
|
+
bytesScanned: 0,
|
|
139
|
+
truncatedFiles: 0,
|
|
140
|
+
missingFiles: 0,
|
|
141
|
+
durationMs: 0,
|
|
142
|
+
toolCalls: 0,
|
|
143
|
+
findingCount: 0,
|
|
144
|
+
inputTokens: 0,
|
|
145
|
+
outputTokens: 0,
|
|
146
|
+
totalTokens: 0,
|
|
147
|
+
providerCostUsd: 0,
|
|
148
|
+
customerCostUsd: null,
|
|
149
|
+
marginUsd: null,
|
|
150
|
+
ledgerEntries: 0,
|
|
151
|
+
actions: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildUsageTelemetry({
|
|
156
|
+
activePersonas = [],
|
|
157
|
+
routing = {},
|
|
158
|
+
byPersona = {},
|
|
159
|
+
fileMetrics = {},
|
|
160
|
+
sessionUsageLedgerEntries = [],
|
|
161
|
+
} = {}) {
|
|
162
|
+
const records = new Map();
|
|
163
|
+
const ensure = ({ personaId = "", agentId = "" } = {}) => {
|
|
164
|
+
const key = personaId || agentId;
|
|
165
|
+
if (!records.has(key)) {
|
|
166
|
+
records.set(key, createUsageRecord({ personaId, agentId }));
|
|
167
|
+
}
|
|
168
|
+
return records.get(key);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const personaId of activePersonas) {
|
|
172
|
+
const record = ensure({ personaId });
|
|
173
|
+
const routed = Array.isArray(routing?.[personaId]) ? routing[personaId] : [];
|
|
174
|
+
const personaRecord = byPersona?.[personaId] || {};
|
|
175
|
+
const visited = Array.isArray(personaRecord.visited) ? personaRecord.visited : [];
|
|
176
|
+
const skipped = Array.isArray(personaRecord.skipped) ? personaRecord.skipped : [];
|
|
177
|
+
const filesForMetrics = visited.length > 0 ? visited : routed;
|
|
178
|
+
const metrics = sumFileMetrics(filesForMetrics, fileMetrics);
|
|
179
|
+
record.routedFiles = routed.length;
|
|
180
|
+
record.visitedFiles = visited.length;
|
|
181
|
+
record.skippedFiles = skipped.length;
|
|
182
|
+
record.filesWithMetrics = metrics.filesWithMetrics;
|
|
183
|
+
record.locScanned = metrics.locScanned;
|
|
184
|
+
record.bytesScanned = metrics.bytesScanned;
|
|
185
|
+
record.truncatedFiles = metrics.truncatedFiles;
|
|
186
|
+
record.missingFiles = metrics.missingFiles;
|
|
187
|
+
record.durationMs = Math.max(0, Math.floor(nonNegativeNumber(personaRecord.durationMs, 0)));
|
|
188
|
+
record.toolCalls = countToolInvocations(personaRecord);
|
|
189
|
+
record.findingCount = Array.isArray(personaRecord.findings) ? personaRecord.findings.length : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const entry of sessionUsageLedgerEntries) {
|
|
193
|
+
const agentId = ledgerString(entry, "agentId");
|
|
194
|
+
const personaId = personaFromAgentId(agentId);
|
|
195
|
+
const record = ensure({ personaId, agentId });
|
|
196
|
+
const inputTokens = ledgerValue(entry, "inputTokens", 0);
|
|
197
|
+
const outputTokens = ledgerValue(entry, "outputTokens", 0);
|
|
198
|
+
const totalTokens = ledgerValue(entry, "totalTokens", inputTokens + outputTokens);
|
|
199
|
+
const providerCost = roundedMoney(
|
|
200
|
+
entry?.ledgerEntry?.providerCostUsd ?? entry?.ledgerEntry?.costUsd ?? entry?.providerCostUsd ?? entry?.costUsd,
|
|
201
|
+
);
|
|
202
|
+
const customerCost = roundedMoney(entry?.ledgerEntry?.customerCostUsd ?? entry?.customerCostUsd);
|
|
203
|
+
record.inputTokens += inputTokens;
|
|
204
|
+
record.outputTokens += outputTokens;
|
|
205
|
+
record.totalTokens += totalTokens;
|
|
206
|
+
if (providerCost != null) {
|
|
207
|
+
record.providerCostUsd = roundedMoney(record.providerCostUsd + providerCost);
|
|
208
|
+
}
|
|
209
|
+
if (customerCost != null) {
|
|
210
|
+
record.customerCostUsd = roundedMoney((record.customerCostUsd || 0) + customerCost);
|
|
211
|
+
}
|
|
212
|
+
if (record.customerCostUsd != null && record.providerCostUsd != null) {
|
|
213
|
+
record.marginUsd = roundedMoney(record.customerCostUsd - record.providerCostUsd);
|
|
214
|
+
}
|
|
215
|
+
record.ledgerEntries += 1;
|
|
216
|
+
const action = ledgerString(entry, "action");
|
|
217
|
+
if (action && !record.actions.includes(action)) record.actions.push(action);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const perAgent = Array.from(records.values()).map((record) => ({
|
|
221
|
+
...record,
|
|
222
|
+
providerCostUsd: roundedMoney(record.providerCostUsd) || 0,
|
|
223
|
+
}));
|
|
224
|
+
const hasCustomerCost = perAgent.some((record) => record.customerCostUsd != null);
|
|
225
|
+
const totals = perAgent.reduce(
|
|
226
|
+
(acc, record) => {
|
|
227
|
+
acc.routedFiles += record.routedFiles;
|
|
228
|
+
acc.visitedFiles += record.visitedFiles;
|
|
229
|
+
acc.filesWithMetrics += record.filesWithMetrics;
|
|
230
|
+
acc.locScanned += record.locScanned;
|
|
231
|
+
acc.bytesScanned += record.bytesScanned;
|
|
232
|
+
acc.durationMs += record.durationMs;
|
|
233
|
+
acc.toolCalls += record.toolCalls;
|
|
234
|
+
acc.findingCount += record.findingCount;
|
|
235
|
+
acc.inputTokens += record.inputTokens;
|
|
236
|
+
acc.outputTokens += record.outputTokens;
|
|
237
|
+
acc.totalTokens += record.totalTokens;
|
|
238
|
+
acc.providerCostUsd += record.providerCostUsd || 0;
|
|
239
|
+
acc.customerCostUsd += record.customerCostUsd || 0;
|
|
240
|
+
acc.ledgerEntries += record.ledgerEntries;
|
|
241
|
+
return acc;
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
routedFiles: 0,
|
|
245
|
+
visitedFiles: 0,
|
|
246
|
+
filesWithMetrics: 0,
|
|
247
|
+
locScanned: 0,
|
|
248
|
+
bytesScanned: 0,
|
|
249
|
+
durationMs: 0,
|
|
250
|
+
toolCalls: 0,
|
|
251
|
+
findingCount: 0,
|
|
252
|
+
inputTokens: 0,
|
|
253
|
+
outputTokens: 0,
|
|
254
|
+
totalTokens: 0,
|
|
255
|
+
providerCostUsd: 0,
|
|
256
|
+
customerCostUsd: 0,
|
|
257
|
+
ledgerEntries: 0,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
totals.providerCostUsd = roundedMoney(totals.providerCostUsd) || 0;
|
|
261
|
+
totals.customerCostUsd = hasCustomerCost ? roundedMoney(totals.customerCostUsd) : null;
|
|
262
|
+
totals.marginUsd = totals.customerCostUsd != null ? roundedMoney(totals.customerCostUsd - totals.providerCostUsd) : null;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
schema: "investor_dd_usage_telemetry_v1",
|
|
266
|
+
totals,
|
|
267
|
+
perAgent,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
59
271
|
function byId(capabilities, id) {
|
|
60
272
|
return capabilities.find((capability) => capability.id === id);
|
|
61
273
|
}
|
|
@@ -110,6 +322,7 @@ export function buildInvestorDdProgress({
|
|
|
110
322
|
artifactFiles = [],
|
|
111
323
|
budgetState = null,
|
|
112
324
|
usageLedgerEntries = [],
|
|
325
|
+
fileMetrics = {},
|
|
113
326
|
} = {}) {
|
|
114
327
|
const activePersonas = uniqueStrings(personas);
|
|
115
328
|
const missingPersonas = INVESTOR_DD_EXPECTED_PERSONAS.filter(
|
|
@@ -169,13 +382,28 @@ export function buildInvestorDdProgress({
|
|
|
169
382
|
devTestBotPhase,
|
|
170
383
|
usageLedgerEntries,
|
|
171
384
|
});
|
|
385
|
+
const usageTelemetry = buildUsageTelemetry({
|
|
386
|
+
activePersonas,
|
|
387
|
+
routing,
|
|
388
|
+
byPersona,
|
|
389
|
+
fileMetrics,
|
|
390
|
+
sessionUsageLedgerEntries,
|
|
391
|
+
});
|
|
172
392
|
const hasSessionUsageLedger = sessionUsageLedgerEntries.length > 0;
|
|
393
|
+
const hasPerAgentTelemetry = usageTelemetry.perAgent.length > 0;
|
|
394
|
+
const hasLocTelemetry = usageTelemetry.totals.locScanned > 0 || usageTelemetry.totals.filesWithMetrics > 0;
|
|
395
|
+
const hasRuntimeTelemetry = usageTelemetry.totals.durationMs > 0;
|
|
396
|
+
const hasCustomerPricing = usageTelemetry.totals.customerCostUsd != null;
|
|
173
397
|
addCapability(capabilities, {
|
|
174
398
|
id: "usage_margin_telemetry",
|
|
175
399
|
label: "Billing-grade per-agent usage, token, time, LOC, and margin telemetry",
|
|
176
|
-
status: compactBudget || hasSessionUsageLedger ? "partial" : dryRun ? "deferred" : "not_configured",
|
|
177
|
-
evidence: compactBudget || hasSessionUsageLedger
|
|
400
|
+
status: compactBudget || hasSessionUsageLedger || hasPerAgentTelemetry ? "partial" : dryRun ? "deferred" : "not_configured",
|
|
401
|
+
evidence: compactBudget || hasSessionUsageLedger || hasPerAgentTelemetry
|
|
178
402
|
? [
|
|
403
|
+
`agentUsageTelemetry=${hasPerAgentTelemetry}`,
|
|
404
|
+
`perAgentUsageRecords=${usageTelemetry.perAgent.length}`,
|
|
405
|
+
`locScanned=${usageTelemetry.totals.locScanned}`,
|
|
406
|
+
`durationMs=${usageTelemetry.totals.durationMs}`,
|
|
179
407
|
...(compactBudget
|
|
180
408
|
? [
|
|
181
409
|
`localBudgetSpentUsd=${compactBudget.spentUsd}`,
|
|
@@ -192,11 +420,12 @@ export function buildInvestorDdProgress({
|
|
|
192
420
|
? ["dryRun=true"]
|
|
193
421
|
: [],
|
|
194
422
|
gaps: [
|
|
195
|
-
hasSessionUsageLedger
|
|
196
|
-
? "only optional DD planner calls are wired to billing-grade session_usage"
|
|
197
|
-
: "
|
|
198
|
-
|
|
199
|
-
|
|
423
|
+
...(hasSessionUsageLedger
|
|
424
|
+
? ["only optional DD planner calls are wired to billing-grade session_usage"]
|
|
425
|
+
: ["billing-grade token/customer-cost telemetry is only available when session_usage ledger entries exist"]),
|
|
426
|
+
...(hasRuntimeTelemetry || dryRun ? [] : ["per-agent runtime is unavailable because no persona execution records were produced"]),
|
|
427
|
+
...(hasLocTelemetry ? [] : ["per-agent LOC is unavailable because file metrics were not collected"]),
|
|
428
|
+
...(hasCustomerPricing ? [] : ["customerCostUsd and marginUsd are unavailable until customer pricing is supplied by the session_usage ledger"]),
|
|
200
429
|
],
|
|
201
430
|
});
|
|
202
431
|
|
|
@@ -277,8 +506,8 @@ export function buildInvestorDdProgress({
|
|
|
277
506
|
});
|
|
278
507
|
|
|
279
508
|
const artifactRequired = dryRun
|
|
280
|
-
? ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html"]
|
|
281
|
-
: ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html", "findings.json"];
|
|
509
|
+
? ["file-metrics.json", "plan.json", "stream.ndjson", "summary.json", "report.md", "report.html"]
|
|
510
|
+
: ["file-metrics.json", "plan.json", "stream.ndjson", "summary.json", "report.md", "report.html", "findings.json"];
|
|
282
511
|
const missingArtifacts = artifactRequired.filter((file) => !artifactFiles.includes(file));
|
|
283
512
|
addCapability(capabilities, {
|
|
284
513
|
id: "artifact_bundle",
|
|
@@ -315,6 +544,7 @@ export function buildInvestorDdProgress({
|
|
|
315
544
|
plannedPersonaCount: INVESTOR_DD_EXPECTED_PERSONAS.length,
|
|
316
545
|
missingPersonas,
|
|
317
546
|
capabilities,
|
|
547
|
+
usageTelemetry,
|
|
318
548
|
routingSummary: {
|
|
319
549
|
routedPersonas: Object.keys(routing || {}).length,
|
|
320
550
|
routedFiles: Object.values(routing || {}).reduce(
|
|
@@ -330,7 +560,7 @@ export function buildInvestorDdProgress({
|
|
|
330
560
|
? ["add the missing frontend/Jules persona to the default Investor-DD roster"]
|
|
331
561
|
: ["run the full 13-persona roster before claiming sellable DD coverage"]),
|
|
332
562
|
"wire Senti session id and live usage counters into Investor-DD runs",
|
|
333
|
-
"
|
|
563
|
+
"wire billing-grade token/customer-price/margin telemetry for every Investor-DD persona",
|
|
334
564
|
"require live reconciliation and report-email proof for sellable DD closeout",
|
|
335
565
|
],
|
|
336
566
|
};
|
|
@@ -52,9 +52,61 @@ function stableStringify(value) {
|
|
|
52
52
|
return JSON.stringify(stableJsonValue(value));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function messageEventCanUseRelaxedContentKey(eventKind) {
|
|
56
|
+
return [
|
|
57
|
+
"agent_response",
|
|
58
|
+
"human_relay",
|
|
59
|
+
"session_message",
|
|
60
|
+
"session_say",
|
|
61
|
+
].includes(keyString(eventKind));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function eventDurabilityScore(event = {}) {
|
|
65
|
+
let score = 0;
|
|
66
|
+
if (readSessionEventSequence(event) > 0) score += 2;
|
|
67
|
+
if (sessionEventHasDurableCursor(event)) score += 1;
|
|
68
|
+
return score;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergeObjectPreferringPrimary(primaryValue, secondaryValue) {
|
|
72
|
+
if (
|
|
73
|
+
primaryValue &&
|
|
74
|
+
typeof primaryValue === "object" &&
|
|
75
|
+
!Array.isArray(primaryValue) &&
|
|
76
|
+
secondaryValue &&
|
|
77
|
+
typeof secondaryValue === "object" &&
|
|
78
|
+
!Array.isArray(secondaryValue)
|
|
79
|
+
) {
|
|
80
|
+
return { ...secondaryValue, ...primaryValue };
|
|
81
|
+
}
|
|
82
|
+
return primaryValue === undefined ? secondaryValue : primaryValue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mergeDuplicateEvent(existingEvent = {}, candidateEvent = {}) {
|
|
86
|
+
const existingScore = eventDurabilityScore(existingEvent);
|
|
87
|
+
const candidateScore = eventDurabilityScore(candidateEvent);
|
|
88
|
+
const candidateIsPrimary = candidateScore > existingScore || (
|
|
89
|
+
candidateScore === existingScore &&
|
|
90
|
+
sessionEventUpgradesExisting(existingEvent, candidateEvent)
|
|
91
|
+
) || (
|
|
92
|
+
candidateScore === existingScore &&
|
|
93
|
+
!sessionEventUpgradesExisting(candidateEvent, existingEvent)
|
|
94
|
+
);
|
|
95
|
+
const primaryEvent = candidateIsPrimary ? candidateEvent : existingEvent;
|
|
96
|
+
const secondaryEvent = candidateIsPrimary ? existingEvent : candidateEvent;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...secondaryEvent,
|
|
100
|
+
...primaryEvent,
|
|
101
|
+
agent: mergeObjectPreferringPrimary(primaryEvent.agent, secondaryEvent.agent),
|
|
102
|
+
payload: mergeObjectPreferringPrimary(primaryEvent.payload, secondaryEvent.payload),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
55
106
|
export function sessionEventIdentityKeys(event = {}) {
|
|
56
107
|
if (!event || typeof event !== "object") return [];
|
|
57
108
|
const keys = [];
|
|
109
|
+
const eventKind = keyString(event.event || event.type);
|
|
58
110
|
const id = keyString(event.id);
|
|
59
111
|
if (id) {
|
|
60
112
|
keys.push(`id:${id}`);
|
|
@@ -109,7 +161,7 @@ export function sessionEventIdentityKeys(event = {}) {
|
|
|
109
161
|
if (message) {
|
|
110
162
|
try {
|
|
111
163
|
keys.push(`content:${stableStringify({
|
|
112
|
-
event:
|
|
164
|
+
event: eventKind,
|
|
113
165
|
agent: keyString(event.agent?.id || event.agentId || payload.agentId || payload.authorId),
|
|
114
166
|
payload: {
|
|
115
167
|
channel: keyString(payload.channel),
|
|
@@ -123,6 +175,22 @@ export function sessionEventIdentityKeys(event = {}) {
|
|
|
123
175
|
} catch {
|
|
124
176
|
// Best-effort duplicate suppression only.
|
|
125
177
|
}
|
|
178
|
+
if (messageEventCanUseRelaxedContentKey(eventKind)) {
|
|
179
|
+
try {
|
|
180
|
+
keys.push(`content-relaxed:${stableStringify({
|
|
181
|
+
event: eventKind,
|
|
182
|
+
agent: keyString(event.agent?.id || event.agentId || payload.agentId || payload.authorId),
|
|
183
|
+
payload: {
|
|
184
|
+
channel: keyString(payload.channel),
|
|
185
|
+
message,
|
|
186
|
+
source: keyString(payload.source),
|
|
187
|
+
},
|
|
188
|
+
ts: timestampKey(event.ts, event.timestamp, event.at),
|
|
189
|
+
})}`);
|
|
190
|
+
} catch {
|
|
191
|
+
// Best-effort duplicate suppression only.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
126
194
|
}
|
|
127
195
|
return keys;
|
|
128
196
|
}
|
|
@@ -151,7 +219,7 @@ export function dedupeSessionEvents(events = []) {
|
|
|
151
219
|
const existingIndex = existingIndexes.length > 0 ? Math.min(...existingIndexes) : -1;
|
|
152
220
|
|
|
153
221
|
if (existingIndex >= 0) {
|
|
154
|
-
deduped[existingIndex] = event;
|
|
222
|
+
deduped[existingIndex] = mergeDuplicateEvent(deduped[existingIndex], event);
|
|
155
223
|
for (const key of keys) {
|
|
156
224
|
indexByKey.set(key, existingIndex);
|
|
157
225
|
}
|