sentinelayer-cli 0.30.7 → 0.30.9
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
|
@@ -4324,8 +4324,9 @@ export function registerSessionCommand(program) {
|
|
|
4324
4324
|
const durablePresenceEnabled = options.presence !== false;
|
|
4325
4325
|
const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4326
4326
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
4327
|
-
const
|
|
4328
|
-
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, 1)
|
|
4327
|
+
const effectivePresenceKeepaliveSeconds =
|
|
4328
|
+
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, intervalSeconds, 1);
|
|
4329
|
+
const presenceKeepaliveMs = effectivePresenceKeepaliveSeconds * 1000;
|
|
4329
4330
|
let lastPresenceHeartbeatMs = 0;
|
|
4330
4331
|
let lastPresenceHeartbeatFingerprint = "";
|
|
4331
4332
|
|
|
@@ -4512,7 +4513,7 @@ export function registerSessionCommand(program) {
|
|
|
4512
4513
|
const publishLifecycle = {
|
|
4513
4514
|
...lifecycle,
|
|
4514
4515
|
presenceIntervalSeconds,
|
|
4515
|
-
presenceKeepaliveSeconds:
|
|
4516
|
+
presenceKeepaliveSeconds: effectivePresenceKeepaliveSeconds,
|
|
4516
4517
|
};
|
|
4517
4518
|
if (lifecycleType === "heartbeat") {
|
|
4518
4519
|
const nowMs = Date.now();
|
|
@@ -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
|
};
|
package/src/session/listeners.js
CHANGED
|
@@ -9,6 +9,7 @@ const LISTENER_EVENT_TYPES = new Set([
|
|
|
9
9
|
// A heartbeat older than this (and not explicitly stopped) means the
|
|
10
10
|
// listener likely died without a clean stop — show it as stale, not live.
|
|
11
11
|
const DEFAULT_STALE_AFTER_MS = 180_000;
|
|
12
|
+
const MAX_STALE_GRACE_MS = 60_000;
|
|
12
13
|
|
|
13
14
|
function normalizeString(value) {
|
|
14
15
|
return String(value || "").trim();
|
|
@@ -29,6 +30,38 @@ function positiveInt(value) {
|
|
|
29
30
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function listenerStaleAfterMs({
|
|
34
|
+
staleAfterMs,
|
|
35
|
+
presenceKeepaliveSeconds,
|
|
36
|
+
presenceIntervalSeconds,
|
|
37
|
+
cadenceSeconds,
|
|
38
|
+
idleIntervalSeconds,
|
|
39
|
+
activeIntervalSeconds,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const fallbackMs = Math.max(1, Number(staleAfterMs) || DEFAULT_STALE_AFTER_MS);
|
|
42
|
+
const keepaliveMs = presenceKeepaliveSeconds ? presenceKeepaliveSeconds * 1000 : null;
|
|
43
|
+
const expectedIntervalSeconds =
|
|
44
|
+
cadenceSeconds ||
|
|
45
|
+
presenceIntervalSeconds ||
|
|
46
|
+
idleIntervalSeconds ||
|
|
47
|
+
activeIntervalSeconds ||
|
|
48
|
+
null;
|
|
49
|
+
const expectedIntervalMs = expectedIntervalSeconds ? expectedIntervalSeconds * 1000 : 0;
|
|
50
|
+
|
|
51
|
+
if (keepaliveMs) {
|
|
52
|
+
// The listener can only publish on a poll tick. Allow one bounded poll
|
|
53
|
+
// interval after the advertised keepalive, not the old 2.5x keepalive
|
|
54
|
+
// window that made dead listeners look live for several extra minutes.
|
|
55
|
+
return keepaliveMs + Math.min(expectedIntervalMs || 0, MAX_STALE_GRACE_MS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (expectedIntervalMs) {
|
|
59
|
+
return Math.max(fallbackMs, Math.round(expectedIntervalMs * 2.5));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return fallbackMs;
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
/**
|
|
33
66
|
* Reduce a stream of session_listener_* events into one row per agent: who is
|
|
34
67
|
* listening, at what cadence, and whether they're currently active or idle.
|
|
@@ -66,17 +99,14 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
66
99
|
const cadenceSeconds = active
|
|
67
100
|
? activeIntervalSeconds || nextPollSeconds
|
|
68
101
|
: idleIntervalSeconds || nextPollSeconds;
|
|
69
|
-
const
|
|
70
|
-
presenceKeepaliveSeconds ||
|
|
71
|
-
presenceIntervalSeconds ||
|
|
72
|
-
cadenceSeconds ||
|
|
73
|
-
idleIntervalSeconds ||
|
|
74
|
-
activeIntervalSeconds ||
|
|
75
|
-
null;
|
|
76
|
-
const staleAfterForRowMs = Math.max(
|
|
102
|
+
const staleAfterForRowMs = listenerStaleAfterMs({
|
|
77
103
|
staleAfterMs,
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
presenceKeepaliveSeconds,
|
|
105
|
+
presenceIntervalSeconds,
|
|
106
|
+
cadenceSeconds,
|
|
107
|
+
idleIntervalSeconds,
|
|
108
|
+
activeIntervalSeconds,
|
|
109
|
+
});
|
|
80
110
|
let status;
|
|
81
111
|
if (stopped) status = "stopped";
|
|
82
112
|
else if (ageMs !== null && ageMs > staleAfterForRowMs) status = "stale";
|
|
@@ -94,6 +124,7 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
94
124
|
presenceIntervalSeconds,
|
|
95
125
|
presenceKeepaliveSeconds,
|
|
96
126
|
nextPollSeconds,
|
|
127
|
+
staleAfterSeconds: Math.round(staleAfterForRowMs / 1000),
|
|
97
128
|
lastSeenAt: epoch ? new Date(epoch).toISOString() : null,
|
|
98
129
|
lastSeenAgoSeconds: ageMs !== null ? Math.round(ageMs / 1000) : null,
|
|
99
130
|
lastHumanActivityAt: normalizeString(payload.lastHumanActivityAt) || null,
|