copilot-usage-studio 0.2.0 → 0.2.2
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/CHANGELOG.md +30 -2
- package/README.md +46 -13
- package/data/github-copilot-pricing.json +34 -6
- package/dist/copilot-usage-studio/browser/chunk-23NLHIOR.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-7I2HDBC6.js +1 -0
- package/dist/copilot-usage-studio/browser/{chunk-TVSYR63W.js → chunk-OS2XPCVW.js} +2 -2
- package/dist/copilot-usage-studio/browser/{chunk-J47KKACI.js → chunk-QMNOUQ2X.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-NXH37FKU.js → chunk-RTOXDOOI.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-TICIZRSM.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
- package/dist/copilot-usage-studio/browser/{chunk-QQOFM3HF.js → chunk-WWU6QMJC.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-XMVGLQQH.js +1 -0
- package/dist/copilot-usage-studio/browser/index.html +2 -2
- package/dist/copilot-usage-studio/browser/main-XFPP45VE.js +1 -0
- package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
- package/docs/local-deployment.md +92 -17
- package/docs/pricing.md +5 -3
- package/docs/scanner-api.md +40 -0
- package/lib/cli.mjs +15 -1
- package/lib/local-runtime.mjs +499 -19
- package/lib/scanner-worker.mjs +25 -0
- package/package.json +20 -2
- package/scripts/pricing-utils.mjs +5 -0
- package/scripts/scan-vscode-sessions.mjs +387 -1475
- package/scripts/scanner-customization-evidence.mjs +613 -0
- package/scripts/scanner-customization-inventory.mjs +1022 -0
- package/scripts/scanner-memory.mjs +229 -0
- package/scripts/scanner-session-parser.mjs +1237 -0
- package/scripts/scanner-traversal.mjs +203 -0
- package/scripts/scanner-workspace.mjs +237 -0
- package/dist/copilot-usage-studio/browser/chunk-CUKTUQ6W.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-EYAHOEVS.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-FXNLFSUB.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-HJNYITFH.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-KDAJN6DF.js +0 -4
- package/dist/copilot-usage-studio/browser/main-TL27ZSEQ.js +0 -1
- package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +0 -1
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { homedir, platform } from 'node:os';
|
|
4
|
-
import { basename, dirname,
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
4
|
import { pathToFileURL } from 'node:url';
|
|
6
5
|
import {
|
|
7
|
-
costBreakdownUsdForTokens,
|
|
8
|
-
costUsdForTokens,
|
|
9
|
-
modelKey,
|
|
10
6
|
normalizeModel,
|
|
11
|
-
pricingModelForModel,
|
|
12
7
|
} from './pricing-utils.mjs';
|
|
8
|
+
import {
|
|
9
|
+
attachMemoryRecalls as attachMemoryRecallsCore,
|
|
10
|
+
createMemoryScanner,
|
|
11
|
+
} from './scanner-memory.mjs';
|
|
12
|
+
import {
|
|
13
|
+
customizationEvidenceFromDebugLogs as customizationEvidenceFromDebugLogsCore,
|
|
14
|
+
mergeCustomizationRecords,
|
|
15
|
+
statusRank,
|
|
16
|
+
} from './scanner-customization-evidence.mjs';
|
|
17
|
+
import { createCustomizationInventoryScanner } from './scanner-customization-inventory.mjs';
|
|
18
|
+
import { createSessionParser } from './scanner-session-parser.mjs';
|
|
19
|
+
import {
|
|
20
|
+
defaultCodeUserDirs,
|
|
21
|
+
listDebugLogFiles as traverseDebugLogFiles,
|
|
22
|
+
listDirs as traverseDirs,
|
|
23
|
+
listFiles as traverseFiles,
|
|
24
|
+
listFilesRecursive as traverseFilesRecursive,
|
|
25
|
+
uniqueResolvedRoots,
|
|
26
|
+
userDirForRoot,
|
|
27
|
+
workspaceDirsForRoot,
|
|
28
|
+
} from './scanner-traversal.mjs';
|
|
29
|
+
import { parseWorkspace as parseWorkspaceEntry } from './scanner-workspace.mjs';
|
|
30
|
+
|
|
31
|
+
export { defaultCodeUserDirs } from './scanner-traversal.mjs';
|
|
13
32
|
|
|
14
33
|
const sessionDataSchemaVersion = 1;
|
|
15
34
|
const pricingData = JSON.parse(
|
|
@@ -42,8 +61,20 @@ function createDiagnostics() {
|
|
|
42
61
|
scannedMemoryRoots: 0,
|
|
43
62
|
importedMemories: 0,
|
|
44
63
|
importedPlans: 0,
|
|
64
|
+
scannedCustomizationRoots: 0,
|
|
65
|
+
scannedCustomizationLocations: [],
|
|
66
|
+
customizationEvidenceScannedSessions: 0,
|
|
67
|
+
customizationEvidenceModelCalls: 0,
|
|
68
|
+
customizationEvidenceTextParts: 0,
|
|
69
|
+
customizationEvidenceMatchedCustomizations: 0,
|
|
70
|
+
customizationEvidenceCapReason: '',
|
|
71
|
+
importedCustomizations: 0,
|
|
72
|
+
workspaceScans: [],
|
|
73
|
+
skippedSystemCustomizations: 0,
|
|
45
74
|
skippedOversizedMemories: 0,
|
|
46
75
|
skippedUnreadableMemories: 0,
|
|
76
|
+
skippedOversizedCustomizations: 0,
|
|
77
|
+
skippedUnreadableCustomizations: 0,
|
|
47
78
|
skippedEmptyDebugLogs: 0,
|
|
48
79
|
skippedChatSnapshotsWithoutRequests: 0,
|
|
49
80
|
skippedDuplicateChatSnapshots: 0,
|
|
@@ -51,24 +82,6 @@ function createDiagnostics() {
|
|
|
51
82
|
};
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
export function defaultCodeUserDirs() {
|
|
55
|
-
const home = homedir();
|
|
56
|
-
|
|
57
|
-
if (platform() === 'win32') {
|
|
58
|
-
const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
59
|
-
return [join(appData, 'Code', 'User'), join(appData, 'Code - Insiders', 'User')];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (platform() === 'darwin') {
|
|
63
|
-
return [
|
|
64
|
-
join(home, 'Library', 'Application Support', 'Code', 'User'),
|
|
65
|
-
join(home, 'Library', 'Application Support', 'Code - Insiders', 'User'),
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return [join(home, '.config', 'Code', 'User'), join(home, '.config', 'Code - Insiders', 'User')];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
85
|
function safeJson(text) {
|
|
73
86
|
try {
|
|
74
87
|
return JSON.parse(text);
|
|
@@ -249,1077 +262,185 @@ function readJsonl(file) {
|
|
|
249
262
|
.filter(Boolean);
|
|
250
263
|
}
|
|
251
264
|
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.map((entry) => join(dir, entry))
|
|
259
|
-
.filter((path) => statSync(path).isDirectory());
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function listFiles(dir, suffix) {
|
|
263
|
-
if (!existsSync(dir)) {
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return readdirSync(dir)
|
|
268
|
-
.map((entry) => join(dir, entry))
|
|
269
|
-
.filter((path) => statSync(path).isFile() && path.endsWith(suffix));
|
|
270
|
-
}
|
|
265
|
+
function stripJsonComments(text) {
|
|
266
|
+
let output = '';
|
|
267
|
+
let inString = false;
|
|
268
|
+
let escaped = false;
|
|
269
|
+
let inLineComment = false;
|
|
270
|
+
let inBlockComment = false;
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
272
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
273
|
+
const character = text[index];
|
|
274
|
+
const next = text[index + 1];
|
|
276
275
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const current = pending.pop();
|
|
282
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
283
|
-
const path = join(current, entry.name);
|
|
284
|
-
if (entry.isDirectory()) {
|
|
285
|
-
pending.push(path);
|
|
286
|
-
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
287
|
-
files.push(path);
|
|
276
|
+
if (inLineComment) {
|
|
277
|
+
if (character === '\n' || character === '\r') {
|
|
278
|
+
inLineComment = false;
|
|
279
|
+
output += character;
|
|
288
280
|
}
|
|
281
|
+
continue;
|
|
289
282
|
}
|
|
290
|
-
}
|
|
291
283
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return [];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const files = [];
|
|
301
|
-
const pending = [root];
|
|
302
|
-
|
|
303
|
-
while (pending.length && files.length < limit) {
|
|
304
|
-
const current = pending.pop();
|
|
305
|
-
let entries = [];
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
entries = readdirSync(current, { withFileTypes: true });
|
|
309
|
-
} catch (error) {
|
|
310
|
-
diagnostics.skippedUnreadableMemories += 1;
|
|
311
|
-
diagnostics.warnings.push(`${current}: memory directory skipped: ${error.message}`);
|
|
284
|
+
if (inBlockComment) {
|
|
285
|
+
if (character === '*' && next === '/') {
|
|
286
|
+
inBlockComment = false;
|
|
287
|
+
index += 1;
|
|
288
|
+
}
|
|
312
289
|
continue;
|
|
313
290
|
}
|
|
314
291
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
} else if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
292
|
+
if (inString) {
|
|
293
|
+
output += character;
|
|
294
|
+
if (escaped) {
|
|
295
|
+
escaped = false;
|
|
296
|
+
} else if (character === '\\') {
|
|
297
|
+
escaped = true;
|
|
298
|
+
} else if (character === '"') {
|
|
299
|
+
inString = false;
|
|
325
300
|
}
|
|
301
|
+
continue;
|
|
326
302
|
}
|
|
327
|
-
}
|
|
328
303
|
|
|
329
|
-
|
|
330
|
-
|
|
304
|
+
if (character === '"') {
|
|
305
|
+
inString = true;
|
|
306
|
+
output += character;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
331
309
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: '';
|
|
338
|
-
} catch {
|
|
339
|
-
return '';
|
|
340
|
-
}
|
|
341
|
-
}
|
|
310
|
+
if (character === '/' && next === '/') {
|
|
311
|
+
inLineComment = true;
|
|
312
|
+
index += 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
342
315
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
316
|
+
if (character === '/' && next === '*') {
|
|
317
|
+
inBlockComment = true;
|
|
318
|
+
index += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
348
321
|
|
|
349
|
-
|
|
350
|
-
return heading.slice(0, 160);
|
|
322
|
+
output += character;
|
|
351
323
|
}
|
|
352
324
|
|
|
353
|
-
return
|
|
354
|
-
.replace(/[-_]+/g, ' ')
|
|
355
|
-
.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
356
|
-
.slice(0, 160);
|
|
325
|
+
return output;
|
|
357
326
|
}
|
|
358
327
|
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.replace(/\s+/g, ' ')
|
|
364
|
-
.trim()
|
|
365
|
-
.slice(0, 280);
|
|
366
|
-
}
|
|
328
|
+
function readJsoncFile(file) {
|
|
329
|
+
if (!existsSync(file)) {
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
367
332
|
|
|
368
|
-
function memoryFromFile(file, root, source, workspace) {
|
|
369
333
|
try {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
diagnostics.skippedOversizedMemories += 1;
|
|
373
|
-
diagnostics.warnings.push(`${file}: memory skipped because it exceeds 1 MiB.`);
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const content = readFileSync(file, 'utf8');
|
|
378
|
-
const relativePath = relative(root, file);
|
|
379
|
-
const segments = relativePath.split(sep).filter(Boolean);
|
|
380
|
-
const sessionId = source === 'workspace' ? decodeMemorySessionId(segments[0]) : '';
|
|
381
|
-
const kind = basename(file).toLowerCase() === 'plan.md' ? 'plan' : 'memory';
|
|
382
|
-
const scope = source === 'global'
|
|
383
|
-
? 'global'
|
|
384
|
-
: segments[0]?.toLowerCase() === 'repo'
|
|
385
|
-
? 'repository'
|
|
386
|
-
: sessionId
|
|
387
|
-
? 'session'
|
|
388
|
-
: 'workspace';
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
id: createHash('sha256').update(resolve(file)).digest('hex').slice(0, 24),
|
|
392
|
-
kind,
|
|
393
|
-
scope,
|
|
394
|
-
title: memoryTitle(content, file),
|
|
395
|
-
excerpt: memoryExcerpt(content),
|
|
396
|
-
content,
|
|
397
|
-
workspace: source === 'global' ? '' : workspace,
|
|
398
|
-
sessionId,
|
|
399
|
-
sourcePath: resolve(file),
|
|
400
|
-
relativePath,
|
|
401
|
-
createdAt: stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : '',
|
|
402
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
403
|
-
sizeBytes: stats.size,
|
|
404
|
-
characterCount: content.length,
|
|
405
|
-
lineCount: content ? content.split(/\r?\n/).length : 0,
|
|
406
|
-
};
|
|
334
|
+
const json = stripJsonComments(readFileSync(file, 'utf8')).replace(/,\s*([}\]])/g, '$1');
|
|
335
|
+
return safeJson(json) ?? {};
|
|
407
336
|
} catch (error) {
|
|
408
|
-
diagnostics.
|
|
409
|
-
|
|
410
|
-
return null;
|
|
337
|
+
diagnostics.warnings.push(`${file}: settings file skipped: ${error.message}`);
|
|
338
|
+
return {};
|
|
411
339
|
}
|
|
412
340
|
}
|
|
413
341
|
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
diagnostics.scannedMemoryRoots += 1;
|
|
420
|
-
const memories = listFilesRecursive(root, (file) => extname(file).toLowerCase() === '.md')
|
|
421
|
-
.map((file) => memoryFromFile(file, root, source, workspace))
|
|
422
|
-
.filter(Boolean);
|
|
423
|
-
|
|
424
|
-
diagnostics.importedMemories += memories.length;
|
|
425
|
-
diagnostics.importedPlans += memories.filter((memory) => memory.kind === 'plan').length;
|
|
426
|
-
return memories;
|
|
342
|
+
function listDirs(dir) {
|
|
343
|
+
return traverseDirs(dir, traversalOptions());
|
|
427
344
|
}
|
|
428
345
|
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
return path.startsWith('/') ? path : `/${path}`;
|
|
346
|
+
function listFiles(dir, suffix) {
|
|
347
|
+
return traverseFiles(dir, suffix, traversalOptions());
|
|
432
348
|
}
|
|
433
349
|
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (memory.scope === 'session') {
|
|
438
|
-
return normalizeMemoryVirtualPath(`/memories/session/${segments.slice(1).join('/')}`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return normalizeMemoryVirtualPath(`/memories/${segments.join('/')}`);
|
|
350
|
+
function listDebugLogFiles(root) {
|
|
351
|
+
return traverseDebugLogFiles(root, traversalOptions());
|
|
442
352
|
}
|
|
443
353
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const recalls = [];
|
|
447
|
-
|
|
448
|
-
for (const file of listDebugLogFiles(sessionDir)) {
|
|
449
|
-
const events = readJsonl(file);
|
|
450
|
-
let modelCallNumber = 0;
|
|
451
|
-
const modelCallNumbers = new Map();
|
|
452
|
-
|
|
453
|
-
events.forEach((event, index) => {
|
|
454
|
-
if (event.type === 'llm_request') {
|
|
455
|
-
modelCallNumber += 1;
|
|
456
|
-
modelCallNumbers.set(index, modelCallNumber);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
events.forEach((event, index) => {
|
|
461
|
-
if (event.type !== 'tool_call' || event.name !== 'memory') {
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const args = safeJson(event.attrs?.args) ?? {};
|
|
466
|
-
if (args.command !== 'view' || !args.path) {
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const nextModelIndex = events.findIndex(
|
|
471
|
-
(candidate, candidateIndex) => candidateIndex > index && candidate.type === 'llm_request',
|
|
472
|
-
);
|
|
473
|
-
const nextModel = nextModelIndex >= 0 ? events[nextModelIndex] : null;
|
|
474
|
-
const tokenFields = nextModel ? llmTokenFields(nextModel) : null;
|
|
475
|
-
const sourceLog = basename(file);
|
|
476
|
-
|
|
477
|
-
recalls.push({
|
|
478
|
-
id: createHash('sha256')
|
|
479
|
-
.update(`${resolve(file)}:${index}:${args.path}`)
|
|
480
|
-
.digest('hex')
|
|
481
|
-
.slice(0, 24),
|
|
482
|
-
sessionId,
|
|
483
|
-
workspace,
|
|
484
|
-
virtualPath: normalizeMemoryVirtualPath(args.path),
|
|
485
|
-
timestamp: timestampForEvent(event),
|
|
486
|
-
sourceLog,
|
|
487
|
-
returnedCharacterCount: String(event.attrs?.result ?? '').length,
|
|
488
|
-
...(nextModel
|
|
489
|
-
? {
|
|
490
|
-
followingModelCall: {
|
|
491
|
-
number: modelCallNumbers.get(nextModelIndex) ?? 0,
|
|
492
|
-
model: normalizeModel(nextModel.attrs?.model, pricing),
|
|
493
|
-
inputTokens: tokenFields.inputTokens,
|
|
494
|
-
cachedInputTokens: tokenFields.cachedInputTokens,
|
|
495
|
-
outputTokens: tokenFields.outputTokens,
|
|
496
|
-
},
|
|
497
|
-
}
|
|
498
|
-
: {}),
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
return recalls.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
354
|
+
function listFilesRecursive(root, predicate, limit = memoryFileLimit, options = {}) {
|
|
355
|
+
return traverseFilesRecursive(root, predicate, limit, traversalOptions(options));
|
|
504
356
|
}
|
|
505
357
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (memory.scope === 'session') {
|
|
516
|
-
return memory.sessionId === recall.sessionId;
|
|
517
|
-
}
|
|
518
|
-
if (memory.scope === 'repository' || memory.scope === 'workspace') {
|
|
519
|
-
return memory.workspace === recall.workspace;
|
|
358
|
+
function traversalOptions(options = {}) {
|
|
359
|
+
return {
|
|
360
|
+
...options,
|
|
361
|
+
onWarning: (message) => diagnostics.warnings.push(message),
|
|
362
|
+
onUnreadable: options.onUnreadable ?? (() => {
|
|
363
|
+
if (options.label === 'customization') {
|
|
364
|
+
diagnostics.skippedUnreadableCustomizations += 1;
|
|
365
|
+
} else {
|
|
366
|
+
diagnostics.skippedUnreadableMemories += 1;
|
|
520
367
|
}
|
|
521
|
-
|
|
522
|
-
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
523
371
|
|
|
524
|
-
|
|
372
|
+
function memoryScanner() {
|
|
373
|
+
return createMemoryScanner({
|
|
374
|
+
diagnostics: () => diagnostics,
|
|
375
|
+
listDebugLogFiles,
|
|
376
|
+
listFilesRecursive,
|
|
377
|
+
llmTokenFields,
|
|
378
|
+
memoryFileLimit,
|
|
379
|
+
memoryFileSizeLimit,
|
|
380
|
+
normalizeModel: (model) => normalizeModel(model, pricing),
|
|
381
|
+
readJsonl,
|
|
382
|
+
safeJson,
|
|
383
|
+
timestampForEvent,
|
|
525
384
|
});
|
|
526
385
|
}
|
|
527
386
|
|
|
528
|
-
function
|
|
529
|
-
return
|
|
387
|
+
function memoriesFromRoot(root, source, workspace = '') {
|
|
388
|
+
return memoryScanner().memoriesFromRoot(root, source, workspace);
|
|
530
389
|
}
|
|
531
390
|
|
|
532
|
-
function
|
|
533
|
-
return
|
|
391
|
+
export function memoryRecallsFromDebugLog(sessionDir, workspace = '') {
|
|
392
|
+
return memoryScanner().memoryRecallsFromDebugLog(sessionDir, workspace);
|
|
534
393
|
}
|
|
535
394
|
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (!existsSync(sourcePath)) {
|
|
540
|
-
return {
|
|
541
|
-
available: false,
|
|
542
|
-
sourcePath: '',
|
|
543
|
-
eventCount: 0,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const eventCount = readJsonl(sourcePath).length;
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
available: true,
|
|
551
|
-
sourcePath,
|
|
552
|
-
eventCount,
|
|
553
|
-
};
|
|
395
|
+
export function attachMemoryRecalls(memories, sessions) {
|
|
396
|
+
return attachMemoryRecallsCore(memories, sessions);
|
|
554
397
|
}
|
|
555
398
|
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
399
|
+
function sessionParser() {
|
|
400
|
+
return createSessionParser({
|
|
401
|
+
diagnostics: () => diagnostics,
|
|
402
|
+
fallbackPricingModel,
|
|
403
|
+
listFiles,
|
|
404
|
+
memoryRecallsFromDebugLog,
|
|
405
|
+
pricing,
|
|
406
|
+
readJsonl,
|
|
407
|
+
safeJson,
|
|
408
|
+
traceEventLimit,
|
|
409
|
+
usdToEur: () => usdToEur,
|
|
410
|
+
workspaceName,
|
|
411
|
+
});
|
|
565
412
|
}
|
|
566
413
|
|
|
567
414
|
export function llmTokenFields(event) {
|
|
568
|
-
|
|
569
|
-
const outputTokens = Number(event.attrs?.outputTokens ?? 0);
|
|
570
|
-
const rawCachedInputTokens = numericAttr(event.attrs, [
|
|
571
|
-
'cachedTokens',
|
|
572
|
-
'cachedInputTokens',
|
|
573
|
-
'cacheReadTokens',
|
|
574
|
-
]);
|
|
575
|
-
const cachedInputTokens = Math.min(inputTokens, rawCachedInputTokens);
|
|
576
|
-
const cacheWriteTokens = numericAttr(event.attrs, ['cacheWriteTokens', 'cachedWriteTokens']);
|
|
577
|
-
const billableInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
578
|
-
|
|
579
|
-
return {
|
|
580
|
-
inputTokens,
|
|
581
|
-
billableInputTokens,
|
|
582
|
-
rawCachedInputTokens,
|
|
583
|
-
cachedInputTokens,
|
|
584
|
-
cacheWriteTokens,
|
|
585
|
-
outputTokens,
|
|
586
|
-
};
|
|
415
|
+
return sessionParser().llmTokenFields(event);
|
|
587
416
|
}
|
|
588
417
|
|
|
589
418
|
export function cacheTokenAuditFromLlmRequests(llmRequests) {
|
|
590
|
-
return
|
|
591
|
-
(audit, event) => {
|
|
592
|
-
const tokenFields = llmTokenFields(event);
|
|
593
|
-
|
|
594
|
-
audit.modelCalls += 1;
|
|
595
|
-
audit.rawInputTokens += tokenFields.inputTokens;
|
|
596
|
-
audit.normalInputTokens += tokenFields.billableInputTokens;
|
|
597
|
-
audit.cachedInputTokens += tokenFields.cachedInputTokens;
|
|
598
|
-
audit.cacheWriteTokens += tokenFields.cacheWriteTokens;
|
|
599
|
-
audit.outputTokens += tokenFields.outputTokens;
|
|
600
|
-
|
|
601
|
-
if (tokenFields.rawCachedInputTokens > 0) {
|
|
602
|
-
audit.callsWithCachedTokens += 1;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (tokenFields.rawCachedInputTokens > tokenFields.inputTokens) {
|
|
606
|
-
audit.invalidCachedTokenSplits += 1;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const rawInputShare =
|
|
610
|
-
tokenFields.inputTokens > 0 ? tokenFields.cachedInputTokens / tokenFields.inputTokens : 0;
|
|
611
|
-
audit.maxCachedInputShare = Math.max(audit.maxCachedInputShare, rawInputShare);
|
|
612
|
-
|
|
613
|
-
return audit;
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
modelCalls: 0,
|
|
617
|
-
callsWithCachedTokens: 0,
|
|
618
|
-
invalidCachedTokenSplits: 0,
|
|
619
|
-
rawInputTokens: 0,
|
|
620
|
-
normalInputTokens: 0,
|
|
621
|
-
cachedInputTokens: 0,
|
|
622
|
-
cacheWriteTokens: 0,
|
|
623
|
-
outputTokens: 0,
|
|
624
|
-
maxCachedInputShare: 0,
|
|
625
|
-
},
|
|
626
|
-
);
|
|
419
|
+
return sessionParser().cacheTokenAuditFromLlmRequests(llmRequests);
|
|
627
420
|
}
|
|
628
421
|
|
|
629
422
|
export function mergeCacheTokenAudits(audits) {
|
|
630
|
-
return
|
|
631
|
-
(total, audit) => ({
|
|
632
|
-
modelCalls: total.modelCalls + audit.modelCalls,
|
|
633
|
-
callsWithCachedTokens: total.callsWithCachedTokens + audit.callsWithCachedTokens,
|
|
634
|
-
invalidCachedTokenSplits: total.invalidCachedTokenSplits + audit.invalidCachedTokenSplits,
|
|
635
|
-
rawInputTokens: total.rawInputTokens + audit.rawInputTokens,
|
|
636
|
-
normalInputTokens: total.normalInputTokens + audit.normalInputTokens,
|
|
637
|
-
cachedInputTokens: total.cachedInputTokens + audit.cachedInputTokens,
|
|
638
|
-
cacheWriteTokens: total.cacheWriteTokens + audit.cacheWriteTokens,
|
|
639
|
-
outputTokens: total.outputTokens + audit.outputTokens,
|
|
640
|
-
maxCachedInputShare: Math.max(total.maxCachedInputShare, audit.maxCachedInputShare),
|
|
641
|
-
}),
|
|
642
|
-
{
|
|
643
|
-
modelCalls: 0,
|
|
644
|
-
callsWithCachedTokens: 0,
|
|
645
|
-
invalidCachedTokenSplits: 0,
|
|
646
|
-
rawInputTokens: 0,
|
|
647
|
-
normalInputTokens: 0,
|
|
648
|
-
cachedInputTokens: 0,
|
|
649
|
-
cacheWriteTokens: 0,
|
|
650
|
-
outputTokens: 0,
|
|
651
|
-
maxCachedInputShare: 0,
|
|
652
|
-
},
|
|
653
|
-
);
|
|
423
|
+
return sessionParser().mergeCacheTokenAudits(audits);
|
|
654
424
|
}
|
|
655
425
|
|
|
656
426
|
export function eventModelCostFields(rawModel, tokenFields) {
|
|
657
|
-
|
|
658
|
-
const pricingModel = pricingModelForModel(normalizedModel, pricing, fallbackPricingModel);
|
|
659
|
-
const tokens = {
|
|
660
|
-
input: tokenFields.billableInputTokens,
|
|
661
|
-
cachedInput: tokenFields.cachedInputTokens,
|
|
662
|
-
cacheWrite: tokenFields.cacheWriteTokens,
|
|
663
|
-
output: tokenFields.outputTokens,
|
|
664
|
-
};
|
|
665
|
-
const costBreakdown = costBreakdownUsd(pricingModel, tokens);
|
|
666
|
-
|
|
667
|
-
return {
|
|
668
|
-
model: normalizedModel,
|
|
669
|
-
rawModel:
|
|
670
|
-
String(rawModel ?? '')
|
|
671
|
-
.replace(/^copilot\//i, '')
|
|
672
|
-
.trim() || 'unknown',
|
|
673
|
-
pricingModel,
|
|
674
|
-
pricingTier: costBreakdown.tier,
|
|
675
|
-
totalTokens: tokenFields.inputTokens + tokenFields.outputTokens + tokenFields.cacheWriteTokens,
|
|
676
|
-
estimatedCost: { usd: costBreakdown.total, eur: costBreakdown.total * usdToEur },
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function parseMaybeJson(value) {
|
|
681
|
-
if (typeof value !== 'string') {
|
|
682
|
-
return value ?? null;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return safeJson(value) ?? value;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function charLength(value) {
|
|
689
|
-
if (value === undefined || value === null) {
|
|
690
|
-
return 0;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return typeof value === 'string' ? value.length : JSON.stringify(value).length;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function parseContentFile(file) {
|
|
697
|
-
const envelope = safeJson(readFileSync(file, 'utf8'));
|
|
698
|
-
const content = parseMaybeJson(envelope?.content ?? envelope);
|
|
699
|
-
|
|
700
|
-
return {
|
|
701
|
-
file,
|
|
702
|
-
content,
|
|
703
|
-
chars: charLength(content),
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function modelCapabilityIndex(sessionDir) {
|
|
708
|
-
const file = join(sessionDir, 'models.json');
|
|
709
|
-
if (!existsSync(file)) {
|
|
710
|
-
return new Map();
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const parsed = safeJson(readFileSync(file, 'utf8'));
|
|
714
|
-
const models = Array.isArray(parsed) ? parsed : [];
|
|
715
|
-
const index = new Map();
|
|
716
|
-
|
|
717
|
-
for (const model of models) {
|
|
718
|
-
const keys = [model?.id, model?.name, model?.version, model?.capabilities?.family]
|
|
719
|
-
.filter(Boolean)
|
|
720
|
-
.map(modelKey);
|
|
721
|
-
|
|
722
|
-
for (const key of keys) {
|
|
723
|
-
if (key) {
|
|
724
|
-
index.set(key, model);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return index;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function modelCapabilityFor(rawModel, capabilityIndex) {
|
|
733
|
-
const key = modelKey(rawModel);
|
|
734
|
-
|
|
735
|
-
return (
|
|
736
|
-
capabilityIndex.get(key) ??
|
|
737
|
-
[...capabilityIndex.entries()].find(([candidate]) => key.includes(candidate))?.[1] ??
|
|
738
|
-
null
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function modelLimitSummaries(sessionDir, llmRequests) {
|
|
743
|
-
const capabilityIndex = modelCapabilityIndex(sessionDir);
|
|
744
|
-
if (!capabilityIndex.size || !llmRequests.length) {
|
|
745
|
-
return [];
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const byModel = new Map();
|
|
749
|
-
|
|
750
|
-
for (const event of llmRequests) {
|
|
751
|
-
const rawModel = String(event.attrs?.model ?? '')
|
|
752
|
-
.replace(/^copilot\//i, '')
|
|
753
|
-
.trim();
|
|
754
|
-
const displayModel = normalizeModel(rawModel, pricing);
|
|
755
|
-
const capability = modelCapabilityFor(rawModel || displayModel, capabilityIndex);
|
|
756
|
-
const limits = capability?.capabilities?.limits ?? {};
|
|
757
|
-
const supports = capability?.capabilities?.supports ?? {};
|
|
758
|
-
const current = byModel.get(displayModel) ?? {
|
|
759
|
-
model: displayModel,
|
|
760
|
-
rawModels: new Set(),
|
|
761
|
-
modelId: capability?.id ?? rawModel,
|
|
762
|
-
vendor: capability?.vendor ?? '',
|
|
763
|
-
tokenizer: capability?.capabilities?.tokenizer ?? '',
|
|
764
|
-
contextWindowTokens: Number(limits.max_context_window_tokens ?? 0) || 0,
|
|
765
|
-
promptLimitTokens: Number(limits.max_prompt_tokens ?? 0) || 0,
|
|
766
|
-
outputLimitTokens: Number(limits.max_output_tokens ?? 0) || 0,
|
|
767
|
-
supportedReasoningEfforts: Array.isArray(supports.reasoning_effort)
|
|
768
|
-
? supports.reasoning_effort
|
|
769
|
-
: [],
|
|
770
|
-
supportedEndpoints: Array.isArray(capability?.supported_endpoints)
|
|
771
|
-
? capability.supported_endpoints
|
|
772
|
-
: [],
|
|
773
|
-
modelPickerEnabled: Boolean(capability?.model_picker_enabled),
|
|
774
|
-
isChatDefault: Boolean(capability?.is_chat_default),
|
|
775
|
-
isChatFallback: Boolean(capability?.is_chat_fallback),
|
|
776
|
-
modelCalls: 0,
|
|
777
|
-
largestRawInputTokens: 0,
|
|
778
|
-
totalRawInputTokens: 0,
|
|
779
|
-
largestOutputTokens: 0,
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
current.rawModels.add(rawModel || 'unknown');
|
|
783
|
-
current.modelCalls += 1;
|
|
784
|
-
current.largestRawInputTokens = Math.max(
|
|
785
|
-
current.largestRawInputTokens,
|
|
786
|
-
Number(event.attrs?.inputTokens ?? 0),
|
|
787
|
-
);
|
|
788
|
-
current.totalRawInputTokens += Number(event.attrs?.inputTokens ?? 0);
|
|
789
|
-
current.largestOutputTokens = Math.max(
|
|
790
|
-
current.largestOutputTokens,
|
|
791
|
-
Number(event.attrs?.outputTokens ?? 0),
|
|
792
|
-
);
|
|
793
|
-
byModel.set(displayModel, current);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
return [...byModel.values()].map((summary) => ({
|
|
797
|
-
...summary,
|
|
798
|
-
rawModels: [...summary.rawModels],
|
|
799
|
-
promptLimitShare:
|
|
800
|
-
summary.promptLimitTokens > 0
|
|
801
|
-
? summary.largestRawInputTokens / summary.promptLimitTokens
|
|
802
|
-
: null,
|
|
803
|
-
contextWindowShare:
|
|
804
|
-
summary.contextWindowTokens > 0
|
|
805
|
-
? summary.largestRawInputTokens / summary.contextWindowTokens
|
|
806
|
-
: null,
|
|
807
|
-
repeatedInputFactor:
|
|
808
|
-
summary.largestRawInputTokens > 0
|
|
809
|
-
? summary.totalRawInputTokens / summary.largestRawInputTokens
|
|
810
|
-
: 0,
|
|
811
|
-
}));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function toolName(tool) {
|
|
815
|
-
return String(tool?.function?.name ?? tool?.name ?? tool?.toolName ?? tool?.id ?? 'unknown_tool');
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function toolSchemaSize(tool) {
|
|
819
|
-
const descriptionChars = charLength(tool?.function?.description ?? tool?.description);
|
|
820
|
-
const parameterChars = charLength(
|
|
821
|
-
tool?.function?.parameters ?? tool?.parameters ?? tool?.input_schema,
|
|
822
|
-
);
|
|
823
|
-
|
|
824
|
-
return {
|
|
825
|
-
name: toolName(tool),
|
|
826
|
-
descriptionChars,
|
|
827
|
-
parameterChars,
|
|
828
|
-
totalChars: charLength(tool),
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function requestOptions(event) {
|
|
833
|
-
const parsed = parseMaybeJson(event.attrs?.requestOptions);
|
|
834
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function reasoningEffort(event) {
|
|
838
|
-
return String(requestOptions(event)?.reasoning?.effort ?? '').trim();
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function textVerbosity(event) {
|
|
842
|
-
return String(requestOptions(event)?.text?.verbosity ?? '').trim();
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function requestShapeMetadata(event) {
|
|
846
|
-
const shape = parseMaybeJson(event.attrs?.requestShape);
|
|
847
|
-
|
|
848
|
-
if (!shape || typeof shape !== 'object') {
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
return {
|
|
853
|
-
api: shape.api ? String(shape.api) : '',
|
|
854
|
-
inputItemCount: Number(shape.inputItemCount ?? 0),
|
|
855
|
-
inputItemTypes: Array.isArray(shape.inputItemTypes)
|
|
856
|
-
? shape.inputItemTypes.filter(Boolean).map(String)
|
|
857
|
-
: [],
|
|
858
|
-
hasPreviousResponseId: Boolean(shape.hasPreviousResponseId),
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function requestShapeSummary(event) {
|
|
863
|
-
const shape = requestShapeMetadata(event);
|
|
864
|
-
if (!shape) {
|
|
865
|
-
return '';
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const parts = [
|
|
869
|
-
shape.api ? `api: ${shape.api}` : '',
|
|
870
|
-
shape.inputItemCount
|
|
871
|
-
? `${shape.inputItemCount.toLocaleString()} input item${shape.inputItemCount === 1 ? '' : 's'}`
|
|
872
|
-
: '',
|
|
873
|
-
shape.inputItemTypes.length ? `types: ${shape.inputItemTypes.join(', ')}` : '',
|
|
874
|
-
shape.hasPreviousResponseId ? 'continues previous response' : '',
|
|
875
|
-
].filter(Boolean);
|
|
876
|
-
|
|
877
|
-
return parts.join(' · ');
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function countedValues(values) {
|
|
881
|
-
const counts = new Map();
|
|
882
|
-
|
|
883
|
-
for (const value of values.filter(Boolean)) {
|
|
884
|
-
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return [...counts.entries()]
|
|
888
|
-
.map(([value, count]) => ({ value, count }))
|
|
889
|
-
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function toolPayloadSummary(toolEvents) {
|
|
893
|
-
const byName = new Map();
|
|
894
|
-
|
|
895
|
-
for (const event of toolEvents) {
|
|
896
|
-
const name = String(
|
|
897
|
-
event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type ?? 'tool',
|
|
898
|
-
);
|
|
899
|
-
const current = byName.get(name) ?? { name, calls: 0, argsChars: 0, resultChars: 0 };
|
|
900
|
-
current.calls += 1;
|
|
901
|
-
current.argsChars += charLength(
|
|
902
|
-
event.attrs?.args ??
|
|
903
|
-
event.attrs?.arguments ??
|
|
904
|
-
event.attrs?.input ??
|
|
905
|
-
event.data?.args ??
|
|
906
|
-
event.data?.arguments ??
|
|
907
|
-
event.data?.input,
|
|
908
|
-
);
|
|
909
|
-
current.resultChars += charLength(
|
|
910
|
-
event.attrs?.result ??
|
|
911
|
-
event.attrs?.output ??
|
|
912
|
-
event.attrs?.stdout ??
|
|
913
|
-
event.data?.result ??
|
|
914
|
-
event.data?.output ??
|
|
915
|
-
event.data?.stdout,
|
|
916
|
-
);
|
|
917
|
-
byName.set(name, current);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
return [...byName.values()]
|
|
921
|
-
.sort(
|
|
922
|
-
(a, b) =>
|
|
923
|
-
b.resultChars + b.argsChars - (a.resultChars + a.argsChars) || a.name.localeCompare(b.name),
|
|
924
|
-
)
|
|
925
|
-
.slice(0, 12);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function requestPayloadSummary(sessionDir, llmRequests, toolEvents) {
|
|
929
|
-
const systemPromptFiles = [
|
|
930
|
-
...new Set(llmRequests.map((event) => event.attrs?.systemPromptFile).filter(Boolean)),
|
|
931
|
-
];
|
|
932
|
-
const toolsFiles = [
|
|
933
|
-
...new Set(llmRequests.map((event) => event.attrs?.toolsFile).filter(Boolean)),
|
|
934
|
-
];
|
|
935
|
-
const systemPrompts = systemPromptFiles
|
|
936
|
-
.map((file) => join(sessionDir, file))
|
|
937
|
-
.filter(existsSync)
|
|
938
|
-
.map(parseContentFile);
|
|
939
|
-
const toolFileSummaries = toolsFiles
|
|
940
|
-
.map((file) => join(sessionDir, file))
|
|
941
|
-
.filter(existsSync)
|
|
942
|
-
.map(parseContentFile);
|
|
943
|
-
const tools = toolFileSummaries.flatMap((summary) =>
|
|
944
|
-
Array.isArray(summary.content) ? summary.content : [],
|
|
945
|
-
);
|
|
946
|
-
const toolSchemas = tools.map(toolSchemaSize);
|
|
947
|
-
const mcpToolNames = toolSchemas
|
|
948
|
-
.map((tool) => tool.name)
|
|
949
|
-
.filter((name) => name.startsWith('mcp_'));
|
|
950
|
-
const reasoningEfforts = countedValues(llmRequests.map(reasoningEffort)).map(
|
|
951
|
-
({ value, count }) => ({
|
|
952
|
-
effort: value,
|
|
953
|
-
count,
|
|
954
|
-
}),
|
|
955
|
-
);
|
|
956
|
-
const subagentLogCount = listFiles(sessionDir, '.jsonl').filter((file) =>
|
|
957
|
-
basename(file).startsWith('runSubagent-'),
|
|
958
|
-
).length;
|
|
959
|
-
|
|
960
|
-
return {
|
|
961
|
-
systemPromptFiles: systemPrompts.length,
|
|
962
|
-
systemPromptChars: systemPrompts.reduce((sum, summary) => sum + summary.chars, 0),
|
|
963
|
-
toolSchemaFiles: toolFileSummaries.length,
|
|
964
|
-
toolSchemaChars: toolFileSummaries.reduce((sum, summary) => sum + summary.chars, 0),
|
|
965
|
-
toolCount: toolSchemas.length,
|
|
966
|
-
mcpToolCount: mcpToolNames.length,
|
|
967
|
-
mcpToolNames: [...new Set(mcpToolNames)].sort(),
|
|
968
|
-
largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 8),
|
|
969
|
-
modelCallsWithSystemPromptFile: llmRequests.filter((event) => event.attrs?.systemPromptFile)
|
|
970
|
-
.length,
|
|
971
|
-
modelCallsWithToolsFile: llmRequests.filter((event) => event.attrs?.toolsFile).length,
|
|
972
|
-
reasoningEfforts,
|
|
973
|
-
toolResultCharsByName: toolPayloadSummary(toolEvents),
|
|
974
|
-
subagentLogCount,
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function contentSummaryFromCache(sessionDir, cache, file) {
|
|
979
|
-
if (!file) {
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
if (cache.has(file)) {
|
|
984
|
-
return cache.get(file);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const path = join(sessionDir, file);
|
|
988
|
-
const summary = existsSync(path) ? parseContentFile(path) : null;
|
|
989
|
-
cache.set(file, summary);
|
|
990
|
-
return summary;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function modelCallSetupPayloadFactory(sessionDir) {
|
|
994
|
-
const cache = new Map();
|
|
995
|
-
|
|
996
|
-
return (event) => {
|
|
997
|
-
if (event.type !== 'llm_request') {
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const systemPromptFile = String(event.attrs?.systemPromptFile ?? '').trim();
|
|
1002
|
-
const toolsFile = String(event.attrs?.toolsFile ?? '').trim();
|
|
1003
|
-
|
|
1004
|
-
if (!systemPromptFile && !toolsFile) {
|
|
1005
|
-
return null;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const systemPrompt = contentSummaryFromCache(sessionDir, cache, systemPromptFile);
|
|
1009
|
-
const toolsSummary = contentSummaryFromCache(sessionDir, cache, toolsFile);
|
|
1010
|
-
const tools = Array.isArray(toolsSummary?.content) ? toolsSummary.content : [];
|
|
1011
|
-
const toolSchemas = tools.map(toolSchemaSize);
|
|
1012
|
-
const mcpToolNames = toolSchemas
|
|
1013
|
-
.map((tool) => tool.name)
|
|
1014
|
-
.filter((name) => name.startsWith('mcp_'));
|
|
1015
|
-
|
|
1016
|
-
return {
|
|
1017
|
-
systemPromptFile,
|
|
1018
|
-
systemPromptChars: systemPrompt?.chars ?? 0,
|
|
1019
|
-
toolsFile,
|
|
1020
|
-
toolSchemaChars: toolsSummary?.chars ?? 0,
|
|
1021
|
-
toolCount: toolSchemas.length,
|
|
1022
|
-
mcpToolCount: mcpToolNames.length,
|
|
1023
|
-
mcpToolNames: [...new Set(mcpToolNames)].sort(),
|
|
1024
|
-
largestToolSchemas: toolSchemas.sort((a, b) => b.totalChars - a.totalChars).slice(0, 5),
|
|
1025
|
-
};
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function debugEvidence(llmRequests, agentResponses) {
|
|
1030
|
-
const inputSeries = llmRequests.map((event) => Number(event.attrs?.inputTokens ?? 0));
|
|
1031
|
-
const outputCaps = [
|
|
1032
|
-
...new Set(
|
|
1033
|
-
llmRequests.map((event) => Number(event.attrs?.maxTokens ?? 0)).filter((value) => value > 0),
|
|
1034
|
-
),
|
|
1035
|
-
].sort((a, b) => a - b);
|
|
1036
|
-
const maxInputTokens = Math.max(0, ...inputSeries);
|
|
1037
|
-
const maxRequestTokens = Math.max(0, ...outputCaps);
|
|
1038
|
-
const reasoningEvents = agentResponses.filter((event) =>
|
|
1039
|
-
String(event.attrs?.reasoning ?? '').trim(),
|
|
1040
|
-
).length;
|
|
1041
|
-
const efforts = countedValues(llmRequests.map(reasoningEffort));
|
|
1042
|
-
const primaryEffort = efforts[0]?.value ?? '';
|
|
1043
|
-
|
|
1044
|
-
return {
|
|
1045
|
-
reasoning: {
|
|
1046
|
-
visible: reasoningEvents > 0 || Boolean(primaryEffort),
|
|
1047
|
-
level: primaryEffort,
|
|
1048
|
-
events: reasoningEvents,
|
|
1049
|
-
source: primaryEffort
|
|
1050
|
-
? 'llm_request.attrs.requestOptions.reasoning.effort'
|
|
1051
|
-
: reasoningEvents > 0
|
|
1052
|
-
? 'agent_response.attrs.reasoning'
|
|
1053
|
-
: '',
|
|
1054
|
-
help: primaryEffort
|
|
1055
|
-
? 'VS Code Agent Debug Logs expose the request reasoning effort in llm_request.attrs.requestOptions.reasoning.effort.'
|
|
1056
|
-
: reasoningEvents > 0
|
|
1057
|
-
? 'VS Code debug logs include reasoning text on agent_response events, but no request reasoning effort was imported.'
|
|
1058
|
-
: 'No reasoning text field was present on imported agent_response events.',
|
|
1059
|
-
},
|
|
1060
|
-
context: {
|
|
1061
|
-
maxInputTokens,
|
|
1062
|
-
maxRequestTokens,
|
|
1063
|
-
outputCaps,
|
|
1064
|
-
requestCapShare: maxRequestTokens > 0 ? maxInputTokens / maxRequestTokens : null,
|
|
1065
|
-
source:
|
|
1066
|
-
maxRequestTokens > 0
|
|
1067
|
-
? 'llm_request.attrs.inputTokens and attrs.maxTokens'
|
|
1068
|
-
: 'llm_request.attrs.inputTokens',
|
|
1069
|
-
help:
|
|
1070
|
-
maxRequestTokens > 0
|
|
1071
|
-
? 'Compares the largest observed input token count with the request maxTokens field present in VS Code debug logs. This is an observed pressure signal, not a provider context-window guarantee.'
|
|
1072
|
-
: 'Largest observed model input token count. The log did not include a request cap to compare against.',
|
|
1073
|
-
},
|
|
1074
|
-
};
|
|
427
|
+
return sessionParser().eventModelCostFields(rawModel, tokenFields);
|
|
1075
428
|
}
|
|
1076
429
|
|
|
1077
430
|
export function modelBreakdownFromLlmRequests(llmRequests) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
for (const event of llmRequests) {
|
|
1081
|
-
const rawModel = String(event.attrs?.model ?? 'unknown')
|
|
1082
|
-
.replace(/^copilot\//i, '')
|
|
1083
|
-
.trim();
|
|
1084
|
-
const displayModel = normalizeModel(rawModel, pricing);
|
|
1085
|
-
const current = byModel.get(displayModel) ?? {
|
|
1086
|
-
model: displayModel,
|
|
1087
|
-
rawModels: new Set(),
|
|
1088
|
-
turns: 0,
|
|
1089
|
-
tokens: { input: 0, cachedInput: 0, cacheWrite: 0, output: 0 },
|
|
1090
|
-
cost: { usd: 0, eur: 0 },
|
|
1091
|
-
costBreakdown: { inputUsd: 0, cachedInputUsd: 0, cacheWriteUsd: 0, outputUsd: 0 },
|
|
1092
|
-
pricingTiers: new Set(),
|
|
1093
|
-
pricingModel: pricingModelForModel(displayModel, pricing, fallbackPricingModel),
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
current.rawModels.add(rawModel || 'unknown');
|
|
1097
|
-
current.turns += 1;
|
|
1098
|
-
const tokenFields = llmTokenFields(event);
|
|
1099
|
-
current.tokens.input += tokenFields.billableInputTokens;
|
|
1100
|
-
current.tokens.cachedInput += tokenFields.cachedInputTokens;
|
|
1101
|
-
current.tokens.cacheWrite += tokenFields.cacheWriteTokens;
|
|
1102
|
-
current.tokens.output += tokenFields.outputTokens;
|
|
1103
|
-
const callCost = costBreakdownUsd(current.pricingModel, {
|
|
1104
|
-
input: tokenFields.billableInputTokens,
|
|
1105
|
-
cachedInput: tokenFields.cachedInputTokens,
|
|
1106
|
-
cacheWrite: tokenFields.cacheWriteTokens,
|
|
1107
|
-
output: tokenFields.outputTokens,
|
|
1108
|
-
});
|
|
1109
|
-
current.costBreakdown.inputUsd += callCost.input;
|
|
1110
|
-
current.costBreakdown.cachedInputUsd += callCost.cachedInput;
|
|
1111
|
-
current.costBreakdown.cacheWriteUsd += callCost.cacheWrite;
|
|
1112
|
-
current.costBreakdown.outputUsd += callCost.output;
|
|
1113
|
-
current.pricingTiers.add(callCost.tier);
|
|
1114
|
-
byModel.set(displayModel, current);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
return [...byModel.values()].map((entry) => {
|
|
1118
|
-
const usd =
|
|
1119
|
-
entry.costBreakdown.inputUsd +
|
|
1120
|
-
entry.costBreakdown.cachedInputUsd +
|
|
1121
|
-
entry.costBreakdown.cacheWriteUsd +
|
|
1122
|
-
entry.costBreakdown.outputUsd;
|
|
1123
|
-
return {
|
|
1124
|
-
...entry,
|
|
1125
|
-
rawModels: [...entry.rawModels],
|
|
1126
|
-
pricingTiers: [...entry.pricingTiers],
|
|
1127
|
-
cost: { usd, eur: usd * usdToEur },
|
|
1128
|
-
};
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
function sessionModelLabel(modelBreakdown, fallbackModel) {
|
|
1133
|
-
if (modelBreakdown.length === 1) {
|
|
1134
|
-
return modelBreakdown[0].model;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
if (modelBreakdown.length > 1) {
|
|
1138
|
-
return `Mixed (${modelBreakdown.map((entry) => entry.model).join(', ')})`;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
return normalizeModel(fallbackModel, pricing);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function estimateTokens(text) {
|
|
1145
|
-
const compact = String(text ?? '').trim();
|
|
1146
|
-
if (!compact) {
|
|
1147
|
-
return 0;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
return Math.max(1, Math.round(Math.max(compact.split(/\s+/).length * 1.35, compact.length / 4)));
|
|
431
|
+
return sessionParser().modelBreakdownFromLlmRequests(llmRequests);
|
|
1151
432
|
}
|
|
1152
433
|
|
|
1153
434
|
function timestampForEvent(event) {
|
|
1154
|
-
return
|
|
435
|
+
return sessionParser().timestampForEvent(event);
|
|
1155
436
|
}
|
|
1156
437
|
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1159
|
-
return `${event.attrs?.model ?? 'model'}: ${Number(event.attrs?.inputTokens ?? 0).toLocaleString()} raw in / ${Number(
|
|
1160
|
-
event.attrs?.outputTokens ?? 0,
|
|
1161
|
-
).toLocaleString()} out`;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
if (String(event.type ?? '').includes('tool')) {
|
|
1165
|
-
return String(event.data?.toolName ?? event.attrs?.toolName ?? event.name ?? event.type);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
if (event.type === 'user_message') {
|
|
1169
|
-
return String(event.attrs?.content ?? '').slice(0, 140);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
if (event.type === 'agent_response') {
|
|
1173
|
-
return parseAssistantResponse(event.attrs?.response).slice(0, 140);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
return String(event.attrs?.details ?? event.name ?? event.type ?? '').slice(0, 140);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function boundedText(value, maxLength = 260) {
|
|
1180
|
-
const compact = String(value ?? '')
|
|
1181
|
-
.replace(/\s+/g, ' ')
|
|
1182
|
-
.trim();
|
|
1183
|
-
return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function summaryValue(value) {
|
|
1187
|
-
if (value === undefined || value === null || value === '') {
|
|
1188
|
-
return '';
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
if (typeof value === 'string') {
|
|
1192
|
-
return boundedText(value);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1196
|
-
return String(value);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
return boundedText(JSON.stringify(compactObject(value)));
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
function sourceEstimatedCost(event) {
|
|
1203
|
-
return summaryValue(event.attrs?.estimatedCost);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function sourceUsageFromNanoAiu(event) {
|
|
1207
|
-
const nanoAiu = Number(event.attrs?.copilotUsageNanoAiu ?? 0);
|
|
1208
|
-
if (!Number.isFinite(nanoAiu) || nanoAiu <= 0) {
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const credits = nanoAiu / 1_000_000_000;
|
|
1213
|
-
|
|
1214
|
-
return {
|
|
1215
|
-
nanoAiu,
|
|
1216
|
-
credits,
|
|
1217
|
-
usd: credits * 0.01,
|
|
1218
|
-
modelCalls: 1,
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
function sourceUsageSummary(llmRequests) {
|
|
1223
|
-
const usages = llmRequests.map(sourceUsageFromNanoAiu).filter(Boolean);
|
|
1224
|
-
const nanoAiu = usages.reduce((sum, usage) => sum + usage.nanoAiu, 0);
|
|
1225
|
-
const credits = nanoAiu / 1_000_000_000;
|
|
1226
|
-
|
|
1227
|
-
return {
|
|
1228
|
-
nanoAiu,
|
|
1229
|
-
credits,
|
|
1230
|
-
usd: credits * 0.01,
|
|
1231
|
-
modelCalls: usages.length,
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function compactObject(value) {
|
|
1236
|
-
if (!value || typeof value !== 'object') {
|
|
1237
|
-
return value;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
const result = {};
|
|
1241
|
-
const skip = new Set(['content', 'response', 'result', 'output', 'stdout', 'stderr']);
|
|
1242
|
-
|
|
1243
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
1244
|
-
if (skip.has(key)) {
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
if (nestedValue === undefined || nestedValue === null) {
|
|
1249
|
-
continue;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
result[key] =
|
|
1253
|
-
typeof nestedValue === 'object'
|
|
1254
|
-
? boundedText(JSON.stringify(nestedValue), 120)
|
|
1255
|
-
: boundedText(nestedValue, 120);
|
|
1256
|
-
|
|
1257
|
-
if (Object.keys(result).length >= 6) {
|
|
1258
|
-
break;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
return result;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
function eventAttributeSummary(event) {
|
|
1266
|
-
const attrs = event.attrs ?? {};
|
|
1267
|
-
const data = event.data ?? {};
|
|
1268
|
-
const candidates = [
|
|
1269
|
-
['category', attrs.category],
|
|
1270
|
-
['source', attrs.source],
|
|
1271
|
-
['model', attrs.model],
|
|
1272
|
-
['debugName', attrs.debugName],
|
|
1273
|
-
['inputTokens', attrs.inputTokens],
|
|
1274
|
-
['cachedTokens', attrs.cachedTokens ?? attrs.cachedInputTokens ?? attrs.cacheReadTokens],
|
|
1275
|
-
['cacheWriteTokens', attrs.cacheWriteTokens ?? attrs.cachedWriteTokens],
|
|
1276
|
-
['outputTokens', attrs.outputTokens],
|
|
1277
|
-
['sourceEstimatedCost', attrs.estimatedCost],
|
|
1278
|
-
['copilotUsageNanoAiu', attrs.copilotUsageNanoAiu],
|
|
1279
|
-
['reasoningEffort', event.type === 'llm_request' ? reasoningEffort(event) : undefined],
|
|
1280
|
-
['textVerbosity', event.type === 'llm_request' ? textVerbosity(event) : undefined],
|
|
1281
|
-
['requestShape', event.type === 'llm_request' ? requestShapeSummary(event) : undefined],
|
|
1282
|
-
['maxTokens', attrs.maxTokens],
|
|
1283
|
-
['ttft', attrs.ttft],
|
|
1284
|
-
['systemPromptFile', attrs.systemPromptFile],
|
|
1285
|
-
['toolsFile', attrs.toolsFile],
|
|
1286
|
-
['vscodeVersion', attrs.vscodeVersion],
|
|
1287
|
-
['copilotVersion', attrs.copilotVersion],
|
|
1288
|
-
['responseId', attrs.responseId],
|
|
1289
|
-
['logVersion', event.v],
|
|
1290
|
-
['toolName', data.toolName ?? attrs.toolName],
|
|
1291
|
-
['details', attrs.details],
|
|
1292
|
-
['content', attrs.content],
|
|
1293
|
-
[
|
|
1294
|
-
'response',
|
|
1295
|
-
event.type === 'agent_response' ? parseAssistantResponse(attrs.response) : undefined,
|
|
1296
|
-
],
|
|
1297
|
-
['data', data && Object.keys(data).length ? data : undefined],
|
|
1298
|
-
];
|
|
1299
|
-
|
|
1300
|
-
const fields = [];
|
|
1301
|
-
const seen = new Set();
|
|
1302
|
-
|
|
1303
|
-
for (const [label, value] of candidates) {
|
|
1304
|
-
const summarized = summaryValue(value);
|
|
1305
|
-
|
|
1306
|
-
if (!summarized || seen.has(label)) {
|
|
1307
|
-
continue;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
fields.push({ label, value: summarized });
|
|
1311
|
-
seen.add(label);
|
|
1312
|
-
|
|
1313
|
-
if (fields.length >= 8) {
|
|
1314
|
-
break;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
return fields;
|
|
438
|
+
export function sessionFromDebugLog(sessionDir, workspaceDir) {
|
|
439
|
+
return sessionParser().sessionFromDebugLog(sessionDir, workspaceDir);
|
|
1319
440
|
}
|
|
1320
441
|
|
|
1321
|
-
function
|
|
1322
|
-
return
|
|
442
|
+
export function sessionFromChatSnapshot(file, workspaceDir) {
|
|
443
|
+
return sessionParser().sessionFromChatSnapshot(file, workspaceDir);
|
|
1323
444
|
}
|
|
1324
445
|
|
|
1325
446
|
function workspaceName(workspaceDir) {
|
|
@@ -1329,397 +450,42 @@ function workspaceName(workspaceDir) {
|
|
|
1329
450
|
return folder ? basename(folder) : basename(workspaceDir);
|
|
1330
451
|
}
|
|
1331
452
|
|
|
1332
|
-
function
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return parsed
|
|
1339
|
-
.flatMap((message) => message?.parts ?? [])
|
|
1340
|
-
.map((part) => {
|
|
1341
|
-
const content =
|
|
1342
|
-
typeof part?.content === 'string'
|
|
1343
|
-
? (safeJson(part.content) ?? part.content)
|
|
1344
|
-
: part?.content;
|
|
1345
|
-
return typeof content === 'object' && content?.text ? content.text : String(content ?? '');
|
|
1346
|
-
})
|
|
1347
|
-
.filter(Boolean)
|
|
1348
|
-
.join('\n');
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
export function sessionFromDebugLog(sessionDir, workspaceDir) {
|
|
1352
|
-
const main = readJsonl(join(sessionDir, 'main.jsonl'));
|
|
1353
|
-
if (!main.length) {
|
|
1354
|
-
diagnostics.skippedEmptyDebugLogs += 1;
|
|
1355
|
-
return null;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const sid = basename(sessionDir);
|
|
1359
|
-
const userMessages = main.filter((event) => event.type === 'user_message');
|
|
1360
|
-
const llmRequests = main.filter((event) => event.type === 'llm_request');
|
|
1361
|
-
const assistantEvents = main.filter(
|
|
1362
|
-
(event) => event.type === 'agent_response' || event.type === 'assistant.message',
|
|
1363
|
-
);
|
|
1364
|
-
const toolEvents = main.filter((event) => String(event.type ?? '').includes('tool'));
|
|
1365
|
-
const errorEvents = main.filter((event) => event.status && event.status !== 'ok');
|
|
1366
|
-
|
|
1367
|
-
if (!userMessages.length && !llmRequests.length && !assistantEvents.length) {
|
|
1368
|
-
diagnostics.skippedEmptyDebugLogs += 1;
|
|
1369
|
-
return null;
|
|
453
|
+
function workspaceFolderPath(workspaceDir) {
|
|
454
|
+
const workspaceJson = join(workspaceDir, 'workspace.json');
|
|
455
|
+
const raw = existsSync(workspaceJson) ? safeJson(readFileSync(workspaceJson, 'utf8')) : null;
|
|
456
|
+
if (!raw?.folder) {
|
|
457
|
+
return '';
|
|
1370
458
|
}
|
|
1371
459
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
(
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const cachedInput = llmRequests.reduce(
|
|
1383
|
-
(sum, event) => sum + llmTokenFields(event).cachedInputTokens,
|
|
1384
|
-
0,
|
|
1385
|
-
);
|
|
1386
|
-
const cacheWrite = llmRequests.reduce(
|
|
1387
|
-
(sum, event) => sum + llmTokenFields(event).cacheWriteTokens,
|
|
1388
|
-
0,
|
|
1389
|
-
);
|
|
1390
|
-
const output = llmRequests.reduce((sum, event) => sum + llmTokenFields(event).outputTokens, 0);
|
|
1391
|
-
const tokens = { input, cachedInput, cacheWrite, output };
|
|
1392
|
-
const usd = modelBreakdown.length
|
|
1393
|
-
? modelBreakdown.reduce((sum, entry) => sum + entry.cost.usd, 0)
|
|
1394
|
-
: costUsd(model, tokens);
|
|
1395
|
-
const startEvent = main.find((event) => event.type === 'session_start') ?? main[0];
|
|
1396
|
-
const debugLogRuntime = {
|
|
1397
|
-
logVersion: Number(startEvent?.v ?? 0) || 0,
|
|
1398
|
-
vscodeVersion: String(startEvent?.attrs?.vscodeVersion ?? '').trim(),
|
|
1399
|
-
copilotVersion: String(startEvent?.attrs?.copilotVersion ?? '').trim(),
|
|
1400
|
-
};
|
|
1401
|
-
const lastEvent = main[main.length - 1];
|
|
1402
|
-
const startedAt =
|
|
1403
|
-
startEvent?.timestamp ??
|
|
1404
|
-
new Date(
|
|
1405
|
-
Number(startEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
|
|
1406
|
-
).toISOString();
|
|
1407
|
-
const endedAt =
|
|
1408
|
-
lastEvent?.timestamp ??
|
|
1409
|
-
new Date(
|
|
1410
|
-
Number(lastEvent?.ts ?? statSync(join(sessionDir, 'main.jsonl')).mtimeMs),
|
|
1411
|
-
).toISOString();
|
|
1412
|
-
const evidence = debugEvidence(llmRequests, assistantEvents, main);
|
|
1413
|
-
const payload = requestPayloadSummary(sessionDir, llmRequests, toolEvents);
|
|
1414
|
-
const modelLimits = modelLimitSummaries(sessionDir, llmRequests);
|
|
1415
|
-
const cacheTokenAudit = cacheTokenAuditFromLlmRequests(llmRequests);
|
|
1416
|
-
const sourceUsage = sourceUsageSummary(llmRequests);
|
|
1417
|
-
const setupPayloadForEvent = modelCallSetupPayloadFactory(sessionDir);
|
|
1418
|
-
const transcript = transcriptAvailability(workspaceDir, sid);
|
|
1419
|
-
const memoryRecalls = memoryRecallsFromDebugLog(sessionDir, workspaceName(workspaceDir));
|
|
1420
|
-
|
|
1421
|
-
if (transcript.available) {
|
|
1422
|
-
diagnostics.debugLogSessionsWithTranscripts += 1;
|
|
1423
|
-
diagnostics.transcriptEventsAvailable += transcript.eventCount;
|
|
460
|
+
try {
|
|
461
|
+
const value = String(raw.folder);
|
|
462
|
+
if (!value.startsWith('file:')) {
|
|
463
|
+
return '';
|
|
464
|
+
}
|
|
465
|
+
const folder = decodeURIComponent(value.replace(/^file:\/+/, ''));
|
|
466
|
+
const resolved = platform() === 'win32' ? folder.replace(/^\//, '') : `/${folder.replace(/^\/+/, '')}`;
|
|
467
|
+
return isAbsolute(resolved) && existsSync(resolved) ? resolved : '';
|
|
468
|
+
} catch {
|
|
469
|
+
return '';
|
|
1424
470
|
}
|
|
471
|
+
}
|
|
1425
472
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
);
|
|
473
|
+
function normalizedWorkspaceFolderScope(workspaceFolders) {
|
|
474
|
+
if (!Array.isArray(workspaceFolders)) {
|
|
475
|
+
return new Set();
|
|
1430
476
|
}
|
|
1431
|
-
|
|
1432
|
-
const turns = [
|
|
1433
|
-
...userMessages.map((event) => ({
|
|
1434
|
-
role: 'user',
|
|
1435
|
-
text: String(event.attrs?.content ?? ''),
|
|
1436
|
-
tokens: estimateTokens(event.attrs?.content),
|
|
1437
|
-
})),
|
|
1438
|
-
...assistantEvents.map((event) => {
|
|
1439
|
-
const text =
|
|
1440
|
-
event.type === 'assistant.message'
|
|
1441
|
-
? event.data?.content
|
|
1442
|
-
: parseAssistantResponse(event.attrs?.response);
|
|
1443
|
-
return { role: 'assistant', text: String(text ?? ''), tokens: estimateTokens(text) };
|
|
1444
|
-
}),
|
|
1445
|
-
].filter((turn) => turn.text.trim());
|
|
1446
|
-
|
|
1447
|
-
return {
|
|
1448
|
-
id: sid,
|
|
1449
|
-
sourceKind: 'vscode-copilot-debug-log',
|
|
1450
|
-
tokenSource: llmRequests.length
|
|
1451
|
-
? 'llm_request_token_totals'
|
|
1452
|
-
: 'debug-log-visible-text-estimate',
|
|
1453
|
-
sessionType: 'Local',
|
|
1454
|
-
location: 'Chat Panel',
|
|
1455
|
-
status: 'Idle',
|
|
1456
|
-
title: firstUserMessage.slice(0, 80),
|
|
1457
|
-
firstPrompt: firstUserMessage.slice(0, 240),
|
|
1458
|
-
workspace: workspaceName(workspaceDir),
|
|
1459
|
-
sourcePath: sessionDir,
|
|
1460
|
-
...(debugLogRuntime.logVersion ||
|
|
1461
|
-
debugLogRuntime.vscodeVersion ||
|
|
1462
|
-
debugLogRuntime.copilotVersion
|
|
1463
|
-
? { debugLogRuntime }
|
|
1464
|
-
: {}),
|
|
1465
|
-
model,
|
|
1466
|
-
modelBreakdown,
|
|
1467
|
-
startedAt,
|
|
1468
|
-
endedAt,
|
|
1469
|
-
tags: ['debug-log', llmRequests.length ? 'llm-request-token-totals' : 'estimated-visible-text'],
|
|
1470
|
-
toolsUsed: [
|
|
1471
|
-
...new Set(
|
|
1472
|
-
toolEvents
|
|
1473
|
-
.map((event) => event.data?.toolName ?? event.attrs?.toolName ?? event.name)
|
|
1474
|
-
.filter(Boolean),
|
|
1475
|
-
),
|
|
1476
|
-
],
|
|
1477
|
-
tokens,
|
|
1478
|
-
cost: { usd, eur: usd * usdToEur },
|
|
1479
|
-
confidence: llmRequests.length ? 'exact' : 'estimated',
|
|
1480
|
-
traceSummary: {
|
|
1481
|
-
modelTurns: llmRequests.length,
|
|
1482
|
-
toolCalls: toolEvents.length,
|
|
1483
|
-
totalTokens: input + cachedInput + cacheWrite + output,
|
|
1484
|
-
errors: errorEvents.length,
|
|
1485
|
-
totalEvents: main.length,
|
|
1486
|
-
reasoningEvents: evidence.reasoning.events,
|
|
1487
|
-
maxInputTokens: evidence.context.maxInputTokens,
|
|
1488
|
-
maxRequestTokens: evidence.context.maxRequestTokens,
|
|
1489
|
-
reasoningEfforts: payload.reasoningEfforts,
|
|
1490
|
-
},
|
|
1491
|
-
cacheTokenAudit,
|
|
1492
|
-
...(sourceUsage.modelCalls > 0 ? { sourceUsage } : {}),
|
|
1493
|
-
transcript,
|
|
1494
|
-
advancedSignals: evidence,
|
|
1495
|
-
requestPayload: payload,
|
|
1496
|
-
modelLimits,
|
|
1497
|
-
...(memoryRecalls.length ? { memoryRecalls } : {}),
|
|
1498
|
-
traceEvents: capTraceEvents(
|
|
1499
|
-
main.map((event, index) => {
|
|
1500
|
-
const tokenFields =
|
|
1501
|
-
event.type === 'llm_request'
|
|
1502
|
-
? llmTokenFields(event)
|
|
1503
|
-
: {
|
|
1504
|
-
inputTokens: 0,
|
|
1505
|
-
billableInputTokens: 0,
|
|
1506
|
-
cachedInputTokens: 0,
|
|
1507
|
-
cacheWriteTokens: 0,
|
|
1508
|
-
outputTokens: 0,
|
|
1509
|
-
};
|
|
1510
|
-
|
|
1511
|
-
const setupPayload = setupPayloadForEvent(event);
|
|
1512
|
-
const sourceUsage = sourceUsageFromNanoAiu(event);
|
|
1513
|
-
const requestShape = event.type === 'llm_request' ? requestShapeMetadata(event) : null;
|
|
1514
|
-
|
|
1515
|
-
return {
|
|
1516
|
-
index,
|
|
1517
|
-
timestamp: timestampForEvent(event),
|
|
1518
|
-
type: String(event.type ?? 'unknown'),
|
|
1519
|
-
name: String(event.name ?? event.type ?? 'unknown'),
|
|
1520
|
-
status: String(event.status ?? 'unknown'),
|
|
1521
|
-
detail: eventDetail(event),
|
|
1522
|
-
attributes: eventAttributeSummary(event),
|
|
1523
|
-
inputTokens: tokenFields.inputTokens,
|
|
1524
|
-
cachedInputTokens: tokenFields.cachedInputTokens,
|
|
1525
|
-
cacheWriteTokens: tokenFields.cacheWriteTokens,
|
|
1526
|
-
outputTokens: tokenFields.outputTokens,
|
|
1527
|
-
ttftMs: event.type === 'llm_request' ? Number(event.attrs?.ttft ?? 0) : 0,
|
|
1528
|
-
maxTokens: event.type === 'llm_request' ? Number(event.attrs?.maxTokens ?? 0) : 0,
|
|
1529
|
-
hasReasoning:
|
|
1530
|
-
event.type === 'agent_response' && Boolean(String(event.attrs?.reasoning ?? '').trim()),
|
|
1531
|
-
reasoningEffort: event.type === 'llm_request' ? reasoningEffort(event) : '',
|
|
1532
|
-
...(requestShape ? { requestShape } : {}),
|
|
1533
|
-
...(event.type === 'llm_request'
|
|
1534
|
-
? eventModelCostFields(event.attrs?.model, tokenFields)
|
|
1535
|
-
: {}),
|
|
1536
|
-
...(event.type === 'llm_request' && sourceEstimatedCost(event)
|
|
1537
|
-
? { sourceEstimatedCost: sourceEstimatedCost(event) }
|
|
1538
|
-
: {}),
|
|
1539
|
-
...(sourceUsage ? { sourceUsage } : {}),
|
|
1540
|
-
...(setupPayload ? { setupPayload } : {}),
|
|
1541
|
-
};
|
|
1542
|
-
}),
|
|
1543
|
-
),
|
|
1544
|
-
turns: turns.slice(0, 60),
|
|
1545
|
-
};
|
|
477
|
+
return new Set(workspaceFolders.map(normalizeWorkspacePath).filter(Boolean));
|
|
1546
478
|
}
|
|
1547
479
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
if (!requests.length) {
|
|
1557
|
-
diagnostics.skippedChatSnapshotsWithoutRequests += 1;
|
|
1558
|
-
return null;
|
|
480
|
+
function normalizeWorkspacePath(path) {
|
|
481
|
+
if (!path) {
|
|
482
|
+
return '';
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
return resolve(String(path)).toLowerCase();
|
|
486
|
+
} catch {
|
|
487
|
+
return '';
|
|
1559
488
|
}
|
|
1560
|
-
|
|
1561
|
-
const firstRequest = requests[0];
|
|
1562
|
-
const firstPrompt =
|
|
1563
|
-
firstRequest?.message?.text ?? snapshot?.customTitle ?? 'Untitled Copilot session';
|
|
1564
|
-
const model = normalizeModel(
|
|
1565
|
-
firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier,
|
|
1566
|
-
pricing,
|
|
1567
|
-
);
|
|
1568
|
-
const output = requests.reduce((sum, request) => sum + Number(request.completionTokens ?? 0), 0);
|
|
1569
|
-
const input = requests.reduce(
|
|
1570
|
-
(sum, request) => sum + estimateTokens(request?.message?.text ?? ''),
|
|
1571
|
-
0,
|
|
1572
|
-
);
|
|
1573
|
-
const tokens = { input, cachedInput: 0, cacheWrite: 0, output };
|
|
1574
|
-
const usd = costUsd(model, tokens);
|
|
1575
|
-
const pricingModel = pricingModelForModel(model, pricing, fallbackPricingModel);
|
|
1576
|
-
const startedAt = new Date(Number(snapshot.creationDate ?? statSync(file).mtimeMs)).toISOString();
|
|
1577
|
-
const endedAt = statSync(file).mtime.toISOString();
|
|
1578
|
-
|
|
1579
|
-
return {
|
|
1580
|
-
id: basename(file, '.jsonl'),
|
|
1581
|
-
sourceKind: 'vscode-chat-session-snapshot',
|
|
1582
|
-
tokenSource: 'chat-snapshot-output-plus-visible-input-estimate',
|
|
1583
|
-
sessionType: 'Local',
|
|
1584
|
-
location: 'Chat Panel',
|
|
1585
|
-
status: 'Idle',
|
|
1586
|
-
title: String(snapshot.customTitle ?? firstPrompt).slice(0, 80),
|
|
1587
|
-
firstPrompt: String(firstPrompt).slice(0, 240),
|
|
1588
|
-
workspace: workspaceName(workspaceDir),
|
|
1589
|
-
sourcePath: file,
|
|
1590
|
-
model,
|
|
1591
|
-
modelBreakdown: [
|
|
1592
|
-
{
|
|
1593
|
-
model,
|
|
1594
|
-
rawModels: [
|
|
1595
|
-
String(
|
|
1596
|
-
firstRequest?.modelId ?? firstRequest?.inputState?.selectedModel?.identifier ?? model,
|
|
1597
|
-
),
|
|
1598
|
-
],
|
|
1599
|
-
turns: requests.length,
|
|
1600
|
-
tokens,
|
|
1601
|
-
cost: { usd, eur: usd * usdToEur },
|
|
1602
|
-
pricingModel,
|
|
1603
|
-
},
|
|
1604
|
-
],
|
|
1605
|
-
startedAt,
|
|
1606
|
-
endedAt,
|
|
1607
|
-
tags: ['chat-session', 'estimated-input'],
|
|
1608
|
-
toolsUsed: [],
|
|
1609
|
-
tokens,
|
|
1610
|
-
cost: { usd, eur: usd * usdToEur },
|
|
1611
|
-
confidence: 'estimated',
|
|
1612
|
-
traceSummary: {
|
|
1613
|
-
modelTurns: requests.length,
|
|
1614
|
-
toolCalls: 0,
|
|
1615
|
-
totalTokens: input + output,
|
|
1616
|
-
errors: 0,
|
|
1617
|
-
totalEvents: records.length,
|
|
1618
|
-
reasoningEvents: 0,
|
|
1619
|
-
maxInputTokens: input,
|
|
1620
|
-
maxRequestTokens: 0,
|
|
1621
|
-
},
|
|
1622
|
-
cacheTokenAudit: {
|
|
1623
|
-
modelCalls: 0,
|
|
1624
|
-
callsWithCachedTokens: 0,
|
|
1625
|
-
invalidCachedTokenSplits: 0,
|
|
1626
|
-
rawInputTokens: 0,
|
|
1627
|
-
normalInputTokens: 0,
|
|
1628
|
-
cachedInputTokens: 0,
|
|
1629
|
-
cacheWriteTokens: 0,
|
|
1630
|
-
outputTokens: 0,
|
|
1631
|
-
maxCachedInputShare: 0,
|
|
1632
|
-
},
|
|
1633
|
-
transcript: {
|
|
1634
|
-
available: false,
|
|
1635
|
-
sourcePath: '',
|
|
1636
|
-
eventCount: 0,
|
|
1637
|
-
},
|
|
1638
|
-
advancedSignals: {
|
|
1639
|
-
reasoning: {
|
|
1640
|
-
visible: false,
|
|
1641
|
-
level: '',
|
|
1642
|
-
events: 0,
|
|
1643
|
-
source: '',
|
|
1644
|
-
help: 'Chat snapshots do not expose agent_response reasoning text or a reasoning-level field.',
|
|
1645
|
-
},
|
|
1646
|
-
context: {
|
|
1647
|
-
maxInputTokens: input,
|
|
1648
|
-
maxRequestTokens: 0,
|
|
1649
|
-
outputCaps: [],
|
|
1650
|
-
requestCapShare: null,
|
|
1651
|
-
source: 'estimated visible chat text',
|
|
1652
|
-
help: 'Chat snapshots only provide visible text context here, so context pressure is not reliable for cost debugging.',
|
|
1653
|
-
},
|
|
1654
|
-
},
|
|
1655
|
-
traceEvents: capTraceEvents(
|
|
1656
|
-
requests.flatMap((request, index) => {
|
|
1657
|
-
const rawRequestModel =
|
|
1658
|
-
request?.modelId ?? request?.inputState?.selectedModel?.identifier ?? model;
|
|
1659
|
-
const userInputTokens = estimateTokens(request?.message?.text ?? '');
|
|
1660
|
-
const assistantOutputTokens = Number(request.completionTokens ?? 0);
|
|
1661
|
-
|
|
1662
|
-
return [
|
|
1663
|
-
{
|
|
1664
|
-
index: index * 2,
|
|
1665
|
-
timestamp: startedAt,
|
|
1666
|
-
type: 'user_message',
|
|
1667
|
-
name: 'user_message',
|
|
1668
|
-
status: 'ok',
|
|
1669
|
-
detail: String(request?.message?.text ?? '').slice(0, 140),
|
|
1670
|
-
inputTokens: userInputTokens,
|
|
1671
|
-
cachedInputTokens: 0,
|
|
1672
|
-
cacheWriteTokens: 0,
|
|
1673
|
-
outputTokens: 0,
|
|
1674
|
-
...eventModelCostFields(rawRequestModel, {
|
|
1675
|
-
inputTokens: userInputTokens,
|
|
1676
|
-
billableInputTokens: userInputTokens,
|
|
1677
|
-
cachedInputTokens: 0,
|
|
1678
|
-
cacheWriteTokens: 0,
|
|
1679
|
-
outputTokens: 0,
|
|
1680
|
-
}),
|
|
1681
|
-
},
|
|
1682
|
-
{
|
|
1683
|
-
index: index * 2 + 1,
|
|
1684
|
-
timestamp: endedAt,
|
|
1685
|
-
type: 'assistant_response',
|
|
1686
|
-
name: 'assistant_response',
|
|
1687
|
-
status: 'ok',
|
|
1688
|
-
detail: `${assistantOutputTokens.toLocaleString()} completion tokens`,
|
|
1689
|
-
inputTokens: 0,
|
|
1690
|
-
cachedInputTokens: 0,
|
|
1691
|
-
cacheWriteTokens: 0,
|
|
1692
|
-
outputTokens: assistantOutputTokens,
|
|
1693
|
-
...eventModelCostFields(rawRequestModel, {
|
|
1694
|
-
inputTokens: 0,
|
|
1695
|
-
billableInputTokens: 0,
|
|
1696
|
-
cachedInputTokens: 0,
|
|
1697
|
-
cacheWriteTokens: 0,
|
|
1698
|
-
outputTokens: assistantOutputTokens,
|
|
1699
|
-
}),
|
|
1700
|
-
},
|
|
1701
|
-
];
|
|
1702
|
-
}),
|
|
1703
|
-
),
|
|
1704
|
-
turns: requests
|
|
1705
|
-
.flatMap((request) => [
|
|
1706
|
-
{
|
|
1707
|
-
role: 'user',
|
|
1708
|
-
text: String(request?.message?.text ?? ''),
|
|
1709
|
-
tokens: estimateTokens(request?.message?.text),
|
|
1710
|
-
},
|
|
1711
|
-
{
|
|
1712
|
-
role: 'assistant',
|
|
1713
|
-
text: (request?.response ?? [])
|
|
1714
|
-
.map((part) => part.value ?? part.generatedTitle ?? part.kind ?? '')
|
|
1715
|
-
.filter(Boolean)
|
|
1716
|
-
.join('\n'),
|
|
1717
|
-
tokens: Number(request.completionTokens ?? 0),
|
|
1718
|
-
},
|
|
1719
|
-
])
|
|
1720
|
-
.filter((turn) => turn.text.trim())
|
|
1721
|
-
.slice(0, 60),
|
|
1722
|
-
};
|
|
1723
489
|
}
|
|
1724
490
|
|
|
1725
491
|
function enrichSessionFromWorkspaceState(session, stateBySessionId) {
|
|
@@ -1767,66 +533,61 @@ function enrichSessionFromWorkspaceState(session, stateBySessionId) {
|
|
|
1767
533
|
};
|
|
1768
534
|
}
|
|
1769
535
|
|
|
1770
|
-
function
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
diagnostics.importedDebugLogSessions += debugSessions.length;
|
|
1781
|
-
|
|
1782
|
-
const chatSessions = listFiles(join(workspaceDir, 'chatSessions'), '.jsonl')
|
|
1783
|
-
.map((file) => {
|
|
1784
|
-
const session = sessionFromChatSnapshot(file, workspaceDir);
|
|
1785
|
-
if (session && debugIds.has(session.id)) {
|
|
1786
|
-
diagnostics.skippedDuplicateChatSnapshots += 1;
|
|
1787
|
-
return null;
|
|
1788
|
-
}
|
|
1789
|
-
return session ? enrichSessionFromWorkspaceState(session, stateBySessionId) : null;
|
|
1790
|
-
})
|
|
1791
|
-
.filter(Boolean);
|
|
1792
|
-
diagnostics.importedChatSnapshotSessions += chatSessions.length;
|
|
536
|
+
function customizationInventoryScanner() {
|
|
537
|
+
return createCustomizationInventoryScanner({
|
|
538
|
+
diagnostics: () => diagnostics,
|
|
539
|
+
listDirs,
|
|
540
|
+
listFilesRecursive,
|
|
541
|
+
readJsonl,
|
|
542
|
+
workspaceFolderPath,
|
|
543
|
+
workspaceName,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
1793
546
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
memories: memoriesFromRoot(
|
|
1797
|
-
join(workspaceDir, 'GitHub.copilot-chat', 'memory-tool', 'memories'),
|
|
1798
|
-
'workspace',
|
|
1799
|
-
workspace,
|
|
1800
|
-
),
|
|
1801
|
-
};
|
|
547
|
+
function customizationsFromWorkspace(workspaceDir, options = {}) {
|
|
548
|
+
return customizationInventoryScanner().customizationsFromWorkspace(workspaceDir, options);
|
|
1802
549
|
}
|
|
1803
550
|
|
|
1804
|
-
function
|
|
1805
|
-
|
|
1806
|
-
return listDirs(workspaceStorage);
|
|
551
|
+
function customizationsFromDiscoveryFolders(debugRoot, workspace, options = {}) {
|
|
552
|
+
return customizationInventoryScanner().customizationsFromDiscoveryFolders(debugRoot, workspace, options);
|
|
1807
553
|
}
|
|
1808
554
|
|
|
1809
|
-
function
|
|
1810
|
-
|
|
1811
|
-
return [root];
|
|
1812
|
-
}
|
|
1813
|
-
if (basename(root) === 'workspaceStorage') {
|
|
1814
|
-
return listDirs(root);
|
|
1815
|
-
}
|
|
1816
|
-
return workspaceDirsFromUserDir(root);
|
|
555
|
+
function customizationsFromDebugReferences(debugRoot, bases, workspace, options = {}) {
|
|
556
|
+
return customizationInventoryScanner().customizationsFromDebugReferences(debugRoot, bases, workspace, options);
|
|
1817
557
|
}
|
|
1818
558
|
|
|
1819
|
-
function
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
559
|
+
function customizationEvidenceFromDebugLogs(
|
|
560
|
+
debugRoot,
|
|
561
|
+
customizations,
|
|
562
|
+
workspace = '',
|
|
563
|
+
workspaceDir = '',
|
|
564
|
+
onProgress = () => {},
|
|
565
|
+
evidenceOptions = {},
|
|
566
|
+
) {
|
|
567
|
+
return customizationEvidenceFromDebugLogsCore(debugRoot, customizations, workspace, workspaceDir, onProgress, {
|
|
568
|
+
...evidenceOptions,
|
|
569
|
+
diagnostics,
|
|
570
|
+
listDirs,
|
|
571
|
+
readJsonl,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function workspaceScannerDependencies() {
|
|
576
|
+
return {
|
|
577
|
+
customizationsFromDebugReferences,
|
|
578
|
+
customizationsFromDiscoveryFolders,
|
|
579
|
+
customizationsFromWorkspace,
|
|
580
|
+
customizationEvidenceFromDebugLogs,
|
|
581
|
+
diagnostics,
|
|
582
|
+
enrichSessionFromWorkspaceState,
|
|
583
|
+
listDirs,
|
|
584
|
+
listFiles,
|
|
585
|
+
memoriesFromRoot,
|
|
586
|
+
readWorkspaceState,
|
|
587
|
+
sessionFromChatSnapshot,
|
|
588
|
+
sessionFromDebugLog,
|
|
589
|
+
workspaceName,
|
|
590
|
+
};
|
|
1830
591
|
}
|
|
1831
592
|
|
|
1832
593
|
/**
|
|
@@ -1843,7 +604,7 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1843
604
|
if (!Array.isArray(configuredRoots)) {
|
|
1844
605
|
throw new TypeError('roots must be an array of VS Code user-data or workspace-storage paths.');
|
|
1845
606
|
}
|
|
1846
|
-
const roots =
|
|
607
|
+
const roots = uniqueResolvedRoots(configuredRoots);
|
|
1847
608
|
const conversionRate = Number(options.usdToEur ?? process.env.USD_TO_EUR ?? 1);
|
|
1848
609
|
if (!Number.isFinite(conversionRate) || conversionRate <= 0) {
|
|
1849
610
|
throw new TypeError('usdToEur must be a positive number.');
|
|
@@ -1860,11 +621,73 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1860
621
|
diagnostics = createDiagnostics();
|
|
1861
622
|
diagnostics.scannedRoots = roots;
|
|
1862
623
|
usdToEur = conversionRate;
|
|
624
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
625
|
+
const workspaceOptions = {
|
|
626
|
+
includeCustomizations: options.includeCustomizations !== false,
|
|
627
|
+
includeSystemCustomizations: options.includeSystemCustomizations === true,
|
|
628
|
+
customizationDiscovery: options.customizationDiscovery ?? null,
|
|
629
|
+
customizationEvidence: options.customizationEvidence ?? {},
|
|
630
|
+
incrementalSince: options.incrementalSince ?? '',
|
|
631
|
+
};
|
|
1863
632
|
|
|
1864
633
|
try {
|
|
634
|
+
onProgress({
|
|
635
|
+
stage: 'roots',
|
|
636
|
+
message: `Scanning ${roots.length} VS Code root${roots.length === 1 ? '' : 's'}.`,
|
|
637
|
+
roots,
|
|
638
|
+
});
|
|
1865
639
|
DatabaseSync = options.sqlite === false ? null : await loadSqliteSupport();
|
|
1866
|
-
|
|
1867
|
-
|
|
640
|
+
let workspaceDirs = [
|
|
641
|
+
...new Set(roots.flatMap((root) => workspaceDirsForRoot(root, traversalOptions()))),
|
|
642
|
+
];
|
|
643
|
+
const workspaceFolderScope = normalizedWorkspaceFolderScope(options.workspaceFolders);
|
|
644
|
+
if (workspaceFolderScope.size) {
|
|
645
|
+
const scopedWorkspaceDirs = dedupeWorkspaceDirsByFolder(
|
|
646
|
+
workspaceDirs.filter((workspaceDir) =>
|
|
647
|
+
workspaceFolderScope.has(normalizeWorkspacePath(workspaceFolderPath(workspaceDir))),
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
onProgress({
|
|
651
|
+
stage: 'workspace-scope',
|
|
652
|
+
message: `Current workspace scope matched ${scopedWorkspaceDirs.length} VS Code storage entr${scopedWorkspaceDirs.length === 1 ? 'y' : 'ies'}.`,
|
|
653
|
+
total: scopedWorkspaceDirs.length,
|
|
654
|
+
});
|
|
655
|
+
workspaceDirs = scopedWorkspaceDirs;
|
|
656
|
+
} else if (options.requireWorkspaceFolders === true) {
|
|
657
|
+
onProgress({
|
|
658
|
+
stage: 'workspace-scope',
|
|
659
|
+
message: 'No current VS Code workspace folder was provided; skipping broad customization evidence scan.',
|
|
660
|
+
total: 0,
|
|
661
|
+
});
|
|
662
|
+
workspaceDirs = [];
|
|
663
|
+
}
|
|
664
|
+
onProgress({
|
|
665
|
+
stage: 'workspaces',
|
|
666
|
+
message: `Found ${workspaceDirs.length} VS Code storage entr${workspaceDirs.length === 1 ? 'y' : 'ies'}.`,
|
|
667
|
+
total: workspaceDirs.length,
|
|
668
|
+
});
|
|
669
|
+
const workspaceResults = [];
|
|
670
|
+
for (const [index, workspaceDir] of workspaceDirs.entries()) {
|
|
671
|
+
if (index > 0 && index % 50 === 0) {
|
|
672
|
+
onProgress({
|
|
673
|
+
stage: 'workspace-queue',
|
|
674
|
+
message: `Checked ${index}/${workspaceDirs.length} VS Code storage entries.`,
|
|
675
|
+
index,
|
|
676
|
+
total: workspaceDirs.length,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
workspaceResults.push(parseWorkspaceEntry(
|
|
680
|
+
workspaceDir,
|
|
681
|
+
{
|
|
682
|
+
...workspaceOptions,
|
|
683
|
+
workspaceIndex: index + 1,
|
|
684
|
+
workspaceTotal: workspaceDirs.length,
|
|
685
|
+
},
|
|
686
|
+
onProgress,
|
|
687
|
+
workspaceScannerDependencies(),
|
|
688
|
+
));
|
|
689
|
+
await yieldToRuntime();
|
|
690
|
+
}
|
|
1868
691
|
const sessions = workspaceResults
|
|
1869
692
|
.flatMap((result) => result.sessions)
|
|
1870
693
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
@@ -1875,6 +698,11 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1875
698
|
.map((userDir) => join(userDir, 'globalStorage', 'github.copilot-chat', 'memory-tool', 'memories')),
|
|
1876
699
|
)];
|
|
1877
700
|
const memoryMap = new Map();
|
|
701
|
+
onProgress({
|
|
702
|
+
stage: 'memories',
|
|
703
|
+
message: `Indexing memories from ${globalMemoryRoots.length} global root${globalMemoryRoots.length === 1 ? '' : 's'} and VS Code storage.`,
|
|
704
|
+
total: globalMemoryRoots.length,
|
|
705
|
+
});
|
|
1878
706
|
for (const memory of [
|
|
1879
707
|
...workspaceResults.flatMap((result) => result.memories),
|
|
1880
708
|
...globalMemoryRoots.flatMap((root) => memoriesFromRoot(root, 'global')),
|
|
@@ -1884,6 +712,19 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1884
712
|
const memories = attachMemoryRecalls([...memoryMap.values()], sessions).sort((a, b) =>
|
|
1885
713
|
b.modifiedAt.localeCompare(a.modifiedAt),
|
|
1886
714
|
);
|
|
715
|
+
const customizationMap = new Map();
|
|
716
|
+
for (const customization of workspaceResults.flatMap((result) => result.customizations)) {
|
|
717
|
+
customizationMap.set(
|
|
718
|
+
customization.id,
|
|
719
|
+
mergeCustomizationRecords(customizationMap.get(customization.id), customization),
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const customizations = [...customizationMap.values()].sort(
|
|
723
|
+
(a, b) =>
|
|
724
|
+
statusRank(b.evidenceStatus) - statusRank(a.evidenceStatus) ||
|
|
725
|
+
b.modifiedAt.localeCompare(a.modifiedAt),
|
|
726
|
+
);
|
|
727
|
+
diagnostics.importedCustomizations = customizations.length;
|
|
1887
728
|
const seenIds = new Set();
|
|
1888
729
|
for (const session of sessions) {
|
|
1889
730
|
if (seenIds.has(session.id)) {
|
|
@@ -1892,6 +733,13 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1892
733
|
seenIds.add(session.id);
|
|
1893
734
|
}
|
|
1894
735
|
|
|
736
|
+
onProgress({
|
|
737
|
+
stage: 'complete',
|
|
738
|
+
message: `Scan complete: imported ${sessions.length} session${sessions.length === 1 ? '' : 's'}.`,
|
|
739
|
+
sessions: sessions.length,
|
|
740
|
+
workspaces: workspaceDirs.length,
|
|
741
|
+
});
|
|
742
|
+
|
|
1895
743
|
return {
|
|
1896
744
|
schemaVersion: sessionDataSchemaVersion,
|
|
1897
745
|
generatedAt,
|
|
@@ -1901,11 +749,15 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1901
749
|
ingestion: {
|
|
1902
750
|
...diagnostics,
|
|
1903
751
|
importedSessions: sessions.length,
|
|
752
|
+
scanKind: options.incrementalSince ? 'incremental' : 'full',
|
|
753
|
+
incrementalSince: options.incrementalSince ?? '',
|
|
754
|
+
customizationEvidenceAnalyzedAt: options.includeCustomizations !== false ? generatedAt : '',
|
|
1904
755
|
cacheTokenAudit: mergeCacheTokenAudits(
|
|
1905
756
|
sessions.map((session) => session.cacheTokenAudit).filter(Boolean),
|
|
1906
757
|
),
|
|
1907
758
|
},
|
|
1908
759
|
memories,
|
|
760
|
+
customizations,
|
|
1909
761
|
sessions,
|
|
1910
762
|
};
|
|
1911
763
|
} finally {
|
|
@@ -1916,6 +768,26 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1916
768
|
}
|
|
1917
769
|
}
|
|
1918
770
|
|
|
771
|
+
function yieldToRuntime() {
|
|
772
|
+
return new Promise((resolveYield) => setImmediate(resolveYield));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function dedupeWorkspaceDirsByFolder(workspaceDirs) {
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
const deduped = [];
|
|
778
|
+
for (const workspaceDir of workspaceDirs) {
|
|
779
|
+
const folder = normalizeWorkspacePath(workspaceFolderPath(workspaceDir));
|
|
780
|
+
const key = folder || normalizeWorkspacePath(workspaceDir);
|
|
781
|
+
if (seen.has(key)) {
|
|
782
|
+
diagnostics.warnings.push(`Duplicate VS Code storage entry skipped for current workspace: ${workspaceDir}`);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
seen.add(key);
|
|
786
|
+
deduped.push(workspaceDir);
|
|
787
|
+
}
|
|
788
|
+
return deduped;
|
|
789
|
+
}
|
|
790
|
+
|
|
1919
791
|
export function writeSessionData(sessionData, outputFile = 'public/data/sessions.json') {
|
|
1920
792
|
const resolvedOutputFile = resolve(outputFile);
|
|
1921
793
|
mkdirSync(dirname(resolvedOutputFile), { recursive: true });
|
|
@@ -1923,16 +795,56 @@ export function writeSessionData(sessionData, outputFile = 'public/data/sessions
|
|
|
1923
795
|
return resolvedOutputFile;
|
|
1924
796
|
}
|
|
1925
797
|
|
|
1926
|
-
export async function runScannerCli(args = process.argv.slice(2), logger = console) {
|
|
1927
|
-
const outputFile = args
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
798
|
+
export async function runScannerCli(args = process.argv.slice(2), logger = console, dependencies = {}) {
|
|
799
|
+
const { outputFile, roots } = parseScannerCliArgs(args);
|
|
800
|
+
const scanner = dependencies.scanner ?? scanVsCodeSessions;
|
|
801
|
+
const writer = dependencies.writer ?? writeSessionData;
|
|
802
|
+
const sessionData = await scanner(roots.length ? { roots } : {});
|
|
803
|
+
const resolvedOutputFile = writer(sessionData, outputFile);
|
|
1931
804
|
|
|
1932
805
|
logger.log(`Wrote ${sessionData.sessions.length} sessions to ${resolvedOutputFile}`);
|
|
1933
806
|
return sessionData;
|
|
1934
807
|
}
|
|
1935
808
|
|
|
809
|
+
function parseScannerCliArgs(args) {
|
|
810
|
+
let outputFile = '';
|
|
811
|
+
const roots = [];
|
|
812
|
+
|
|
813
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
814
|
+
const argument = args[index];
|
|
815
|
+
const [flag, inlineValue] = argument.split('=', 2);
|
|
816
|
+
if (flag === '--output' || flag === '--root') {
|
|
817
|
+
const value = inlineValue ?? args[index + 1];
|
|
818
|
+
if (!value) {
|
|
819
|
+
throw new Error(`${flag} requires a value.`);
|
|
820
|
+
}
|
|
821
|
+
if (inlineValue === undefined) {
|
|
822
|
+
index += 1;
|
|
823
|
+
}
|
|
824
|
+
if (flag === '--output') {
|
|
825
|
+
outputFile = value;
|
|
826
|
+
} else {
|
|
827
|
+
roots.push(value);
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (argument.startsWith('-')) {
|
|
833
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
834
|
+
}
|
|
835
|
+
if (!outputFile) {
|
|
836
|
+
outputFile = argument;
|
|
837
|
+
} else {
|
|
838
|
+
roots.push(argument);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
outputFile: outputFile || 'public/data/sessions.json',
|
|
844
|
+
roots,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
1936
848
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1937
849
|
await runScannerCli();
|
|
1938
850
|
}
|