copilot-usage-studio 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +42 -13
- package/data/github-copilot-pricing.json +34 -6
- package/dist/copilot-usage-studio/browser/chunk-2SAR2Q6M.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-EWN3YOHT.js +1 -0
- package/dist/copilot-usage-studio/browser/{chunk-NXH37FKU.js → chunk-KS5W2HMH.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-TVSYR63W.js → chunk-OS2XPCVW.js} +2 -2
- package/dist/copilot-usage-studio/browser/{chunk-QQOFM3HF.js → chunk-U4H425LY.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-VEKXIM47.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
- package/dist/copilot-usage-studio/browser/{chunk-J47KKACI.js → chunk-XHID4XVE.js} +1 -1
- package/dist/copilot-usage-studio/browser/index.html +2 -2
- package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +1 -0
- package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
- package/docs/local-deployment.md +67 -11
- package/docs/pricing.md +3 -3
- package/docs/scanner-api.md +28 -0
- package/lib/cli.mjs +15 -1
- package/lib/local-runtime.mjs +455 -19
- package/lib/scanner-worker.mjs +25 -0
- package/package.json +17 -1
- package/scripts/pricing-utils.mjs +5 -0
- package/scripts/scan-vscode-sessions.mjs +383 -1475
- package/scripts/scanner-customization-evidence.mjs +576 -0
- package/scripts/scanner-customization-inventory.mjs +1021 -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 +209 -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,72 @@ 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
|
+
};
|
|
1863
631
|
|
|
1864
632
|
try {
|
|
633
|
+
onProgress({
|
|
634
|
+
stage: 'roots',
|
|
635
|
+
message: `Scanning ${roots.length} VS Code root${roots.length === 1 ? '' : 's'}.`,
|
|
636
|
+
roots,
|
|
637
|
+
});
|
|
1865
638
|
DatabaseSync = options.sqlite === false ? null : await loadSqliteSupport();
|
|
1866
|
-
|
|
1867
|
-
|
|
639
|
+
let workspaceDirs = [
|
|
640
|
+
...new Set(roots.flatMap((root) => workspaceDirsForRoot(root, traversalOptions()))),
|
|
641
|
+
];
|
|
642
|
+
const workspaceFolderScope = normalizedWorkspaceFolderScope(options.workspaceFolders);
|
|
643
|
+
if (workspaceFolderScope.size) {
|
|
644
|
+
const scopedWorkspaceDirs = dedupeWorkspaceDirsByFolder(
|
|
645
|
+
workspaceDirs.filter((workspaceDir) =>
|
|
646
|
+
workspaceFolderScope.has(normalizeWorkspacePath(workspaceFolderPath(workspaceDir))),
|
|
647
|
+
),
|
|
648
|
+
);
|
|
649
|
+
onProgress({
|
|
650
|
+
stage: 'workspace-scope',
|
|
651
|
+
message: `Current workspace scope matched ${scopedWorkspaceDirs.length} VS Code storage entr${scopedWorkspaceDirs.length === 1 ? 'y' : 'ies'}.`,
|
|
652
|
+
total: scopedWorkspaceDirs.length,
|
|
653
|
+
});
|
|
654
|
+
workspaceDirs = scopedWorkspaceDirs;
|
|
655
|
+
} else if (options.requireWorkspaceFolders === true) {
|
|
656
|
+
onProgress({
|
|
657
|
+
stage: 'workspace-scope',
|
|
658
|
+
message: 'No current VS Code workspace folder was provided; skipping broad customization evidence scan.',
|
|
659
|
+
total: 0,
|
|
660
|
+
});
|
|
661
|
+
workspaceDirs = [];
|
|
662
|
+
}
|
|
663
|
+
onProgress({
|
|
664
|
+
stage: 'workspaces',
|
|
665
|
+
message: `Found ${workspaceDirs.length} VS Code storage entr${workspaceDirs.length === 1 ? 'y' : 'ies'}.`,
|
|
666
|
+
total: workspaceDirs.length,
|
|
667
|
+
});
|
|
668
|
+
const workspaceResults = [];
|
|
669
|
+
for (const [index, workspaceDir] of workspaceDirs.entries()) {
|
|
670
|
+
if (index > 0 && index % 50 === 0) {
|
|
671
|
+
onProgress({
|
|
672
|
+
stage: 'workspace-queue',
|
|
673
|
+
message: `Checked ${index}/${workspaceDirs.length} VS Code storage entries.`,
|
|
674
|
+
index,
|
|
675
|
+
total: workspaceDirs.length,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
workspaceResults.push(parseWorkspaceEntry(
|
|
679
|
+
workspaceDir,
|
|
680
|
+
{
|
|
681
|
+
...workspaceOptions,
|
|
682
|
+
workspaceIndex: index + 1,
|
|
683
|
+
workspaceTotal: workspaceDirs.length,
|
|
684
|
+
},
|
|
685
|
+
onProgress,
|
|
686
|
+
workspaceScannerDependencies(),
|
|
687
|
+
));
|
|
688
|
+
await yieldToRuntime();
|
|
689
|
+
}
|
|
1868
690
|
const sessions = workspaceResults
|
|
1869
691
|
.flatMap((result) => result.sessions)
|
|
1870
692
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
@@ -1875,6 +697,11 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1875
697
|
.map((userDir) => join(userDir, 'globalStorage', 'github.copilot-chat', 'memory-tool', 'memories')),
|
|
1876
698
|
)];
|
|
1877
699
|
const memoryMap = new Map();
|
|
700
|
+
onProgress({
|
|
701
|
+
stage: 'memories',
|
|
702
|
+
message: `Indexing memories from ${globalMemoryRoots.length} global root${globalMemoryRoots.length === 1 ? '' : 's'} and VS Code storage.`,
|
|
703
|
+
total: globalMemoryRoots.length,
|
|
704
|
+
});
|
|
1878
705
|
for (const memory of [
|
|
1879
706
|
...workspaceResults.flatMap((result) => result.memories),
|
|
1880
707
|
...globalMemoryRoots.flatMap((root) => memoriesFromRoot(root, 'global')),
|
|
@@ -1884,6 +711,19 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1884
711
|
const memories = attachMemoryRecalls([...memoryMap.values()], sessions).sort((a, b) =>
|
|
1885
712
|
b.modifiedAt.localeCompare(a.modifiedAt),
|
|
1886
713
|
);
|
|
714
|
+
const customizationMap = new Map();
|
|
715
|
+
for (const customization of workspaceResults.flatMap((result) => result.customizations)) {
|
|
716
|
+
customizationMap.set(
|
|
717
|
+
customization.id,
|
|
718
|
+
mergeCustomizationRecords(customizationMap.get(customization.id), customization),
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const customizations = [...customizationMap.values()].sort(
|
|
722
|
+
(a, b) =>
|
|
723
|
+
statusRank(b.evidenceStatus) - statusRank(a.evidenceStatus) ||
|
|
724
|
+
b.modifiedAt.localeCompare(a.modifiedAt),
|
|
725
|
+
);
|
|
726
|
+
diagnostics.importedCustomizations = customizations.length;
|
|
1887
727
|
const seenIds = new Set();
|
|
1888
728
|
for (const session of sessions) {
|
|
1889
729
|
if (seenIds.has(session.id)) {
|
|
@@ -1892,6 +732,13 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1892
732
|
seenIds.add(session.id);
|
|
1893
733
|
}
|
|
1894
734
|
|
|
735
|
+
onProgress({
|
|
736
|
+
stage: 'complete',
|
|
737
|
+
message: `Scan complete: imported ${sessions.length} session${sessions.length === 1 ? '' : 's'}.`,
|
|
738
|
+
sessions: sessions.length,
|
|
739
|
+
workspaces: workspaceDirs.length,
|
|
740
|
+
});
|
|
741
|
+
|
|
1895
742
|
return {
|
|
1896
743
|
schemaVersion: sessionDataSchemaVersion,
|
|
1897
744
|
generatedAt,
|
|
@@ -1906,6 +753,7 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1906
753
|
),
|
|
1907
754
|
},
|
|
1908
755
|
memories,
|
|
756
|
+
customizations,
|
|
1909
757
|
sessions,
|
|
1910
758
|
};
|
|
1911
759
|
} finally {
|
|
@@ -1916,6 +764,26 @@ export async function scanVsCodeSessions(options = {}) {
|
|
|
1916
764
|
}
|
|
1917
765
|
}
|
|
1918
766
|
|
|
767
|
+
function yieldToRuntime() {
|
|
768
|
+
return new Promise((resolveYield) => setImmediate(resolveYield));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function dedupeWorkspaceDirsByFolder(workspaceDirs) {
|
|
772
|
+
const seen = new Set();
|
|
773
|
+
const deduped = [];
|
|
774
|
+
for (const workspaceDir of workspaceDirs) {
|
|
775
|
+
const folder = normalizeWorkspacePath(workspaceFolderPath(workspaceDir));
|
|
776
|
+
const key = folder || normalizeWorkspacePath(workspaceDir);
|
|
777
|
+
if (seen.has(key)) {
|
|
778
|
+
diagnostics.warnings.push(`Duplicate VS Code storage entry skipped for current workspace: ${workspaceDir}`);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
seen.add(key);
|
|
782
|
+
deduped.push(workspaceDir);
|
|
783
|
+
}
|
|
784
|
+
return deduped;
|
|
785
|
+
}
|
|
786
|
+
|
|
1919
787
|
export function writeSessionData(sessionData, outputFile = 'public/data/sessions.json') {
|
|
1920
788
|
const resolvedOutputFile = resolve(outputFile);
|
|
1921
789
|
mkdirSync(dirname(resolvedOutputFile), { recursive: true });
|
|
@@ -1923,16 +791,56 @@ export function writeSessionData(sessionData, outputFile = 'public/data/sessions
|
|
|
1923
791
|
return resolvedOutputFile;
|
|
1924
792
|
}
|
|
1925
793
|
|
|
1926
|
-
export async function runScannerCli(args = process.argv.slice(2), logger = console) {
|
|
1927
|
-
const outputFile = args
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
794
|
+
export async function runScannerCli(args = process.argv.slice(2), logger = console, dependencies = {}) {
|
|
795
|
+
const { outputFile, roots } = parseScannerCliArgs(args);
|
|
796
|
+
const scanner = dependencies.scanner ?? scanVsCodeSessions;
|
|
797
|
+
const writer = dependencies.writer ?? writeSessionData;
|
|
798
|
+
const sessionData = await scanner(roots.length ? { roots } : {});
|
|
799
|
+
const resolvedOutputFile = writer(sessionData, outputFile);
|
|
1931
800
|
|
|
1932
801
|
logger.log(`Wrote ${sessionData.sessions.length} sessions to ${resolvedOutputFile}`);
|
|
1933
802
|
return sessionData;
|
|
1934
803
|
}
|
|
1935
804
|
|
|
805
|
+
function parseScannerCliArgs(args) {
|
|
806
|
+
let outputFile = '';
|
|
807
|
+
const roots = [];
|
|
808
|
+
|
|
809
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
810
|
+
const argument = args[index];
|
|
811
|
+
const [flag, inlineValue] = argument.split('=', 2);
|
|
812
|
+
if (flag === '--output' || flag === '--root') {
|
|
813
|
+
const value = inlineValue ?? args[index + 1];
|
|
814
|
+
if (!value) {
|
|
815
|
+
throw new Error(`${flag} requires a value.`);
|
|
816
|
+
}
|
|
817
|
+
if (inlineValue === undefined) {
|
|
818
|
+
index += 1;
|
|
819
|
+
}
|
|
820
|
+
if (flag === '--output') {
|
|
821
|
+
outputFile = value;
|
|
822
|
+
} else {
|
|
823
|
+
roots.push(value);
|
|
824
|
+
}
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (argument.startsWith('-')) {
|
|
829
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
830
|
+
}
|
|
831
|
+
if (!outputFile) {
|
|
832
|
+
outputFile = argument;
|
|
833
|
+
} else {
|
|
834
|
+
roots.push(argument);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
outputFile: outputFile || 'public/data/sessions.json',
|
|
840
|
+
roots,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
1936
844
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1937
845
|
await runScannerCli();
|
|
1938
846
|
}
|