@kentwynn/kgraph 0.2.4 → 0.2.5
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/dist/cli/commands/doctor.js +21 -1
- package/dist/cognition/cognition-quality.d.ts +2 -1
- package/dist/cognition/cognition-quality.js +37 -6
- package/dist/cognition/cognition-updater.js +10 -1
- package/dist/cognition/markdown-note-parser.js +6 -1
- package/dist/context/context-query.js +74 -6
- package/dist/session/session-store.js +12 -2
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { analyzeCognitionQuality, } from '../../cognition/cognition-quality.js';
|
|
4
4
|
import { loadConfig } from '../../config/config.js';
|
|
5
5
|
import { listIntegrations } from '../../integrations/integration-store.js';
|
|
6
|
+
import { getCurrentCommit, isGitRepo } from '../../scanner/git-utils.js';
|
|
6
7
|
import { assertWorkspace, pathExists, resolveWorkspace, } from '../../storage/kgraph-paths.js';
|
|
7
8
|
import { mapPaths, mapsExist, readMaps } from '../../storage/map-store.js';
|
|
8
9
|
import { runCommand } from '../errors.js';
|
|
@@ -37,7 +38,9 @@ export function registerDoctorCommand(program) {
|
|
|
37
38
|
checks.push({
|
|
38
39
|
label: 'maps',
|
|
39
40
|
ok: mapStatus,
|
|
40
|
-
detail: mapStatus
|
|
41
|
+
detail: mapStatus
|
|
42
|
+
? 'structural maps are present'
|
|
43
|
+
: 'run `kgraph scan` or just `kgraph`',
|
|
41
44
|
});
|
|
42
45
|
const maps = mapStatus ? await readMaps(workspace) : undefined;
|
|
43
46
|
if (maps) {
|
|
@@ -46,6 +49,19 @@ export function registerDoctorCommand(program) {
|
|
|
46
49
|
ok: true,
|
|
47
50
|
detail: `${maps.fileMap.files.length} files, ${maps.symbolMap.symbols.length} symbols, ${maps.dependencyMap.dependencies.length} dependencies`,
|
|
48
51
|
});
|
|
52
|
+
// Detect whether the repo has advanced past the commit that was scanned
|
|
53
|
+
if (maps.fileMap.scannedAtCommit && (await isGitRepo(rootPath))) {
|
|
54
|
+
const headCommit = await getCurrentCommit(rootPath);
|
|
55
|
+
const stale = headCommit !== null &&
|
|
56
|
+
headCommit !== maps.fileMap.scannedAtCommit;
|
|
57
|
+
checks.push({
|
|
58
|
+
label: 'scan freshness',
|
|
59
|
+
ok: !stale,
|
|
60
|
+
detail: stale
|
|
61
|
+
? `scanned at ${maps.fileMap.scannedAtCommit.slice(0, 7)}, HEAD is ${headCommit.slice(0, 7)} — run \`kgraph scan\``
|
|
62
|
+
: `maps current at ${maps.fileMap.scannedAtCommit.slice(0, 7)}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
else {
|
|
51
67
|
const paths = mapPaths(workspace);
|
|
@@ -125,6 +141,7 @@ function printChecks(checks) {
|
|
|
125
141
|
export function printQualityReport(report) {
|
|
126
142
|
console.log(`Notes: ${report.noteCount}`);
|
|
127
143
|
console.log(`Mixed/stale/unresolved notes: ${report.mixedOrStaleCount}`);
|
|
144
|
+
console.log(`Orphaned notes (all refs dead): ${report.orphanedNoteCount}`);
|
|
128
145
|
console.log(`Noisy file refs: ${report.noisyFileRefCount}`);
|
|
129
146
|
console.log(`Noisy symbol refs: ${report.noisySymbolRefCount}`);
|
|
130
147
|
console.log(`Unresolved local imports: ${report.unresolvedLocalImportCount}`);
|
|
@@ -152,6 +169,9 @@ export function printQualityReport(report) {
|
|
|
152
169
|
}
|
|
153
170
|
function summarizeQualityFindings(report) {
|
|
154
171
|
const findings = [];
|
|
172
|
+
if (report.orphanedNoteCount > 0) {
|
|
173
|
+
findings.push(`${report.orphanedNoteCount} orphaned cognition note(s) (all refs dead); run \`kgraph repair\` to archive`);
|
|
174
|
+
}
|
|
155
175
|
if (report.mixedOrStaleCount > 0) {
|
|
156
176
|
findings.push(`${report.mixedOrStaleCount} stale/mixed/unresolved note(s)`);
|
|
157
177
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { KGraphWorkspace } from '../types/config.js';
|
|
2
1
|
import type { ReferenceStatus } from '../types/cognition.js';
|
|
2
|
+
import type { KGraphWorkspace } from '../types/config.js';
|
|
3
3
|
import type { DependencyMap, FileMap, RelationshipMap, SymbolMap } from '../types/maps.js';
|
|
4
4
|
export interface CognitionRepairChange {
|
|
5
5
|
noteId: string;
|
|
@@ -21,6 +21,7 @@ export interface CognitionQualityReport {
|
|
|
21
21
|
sessionRepeatedReadCount: number;
|
|
22
22
|
sessionEstimatedReadTokens: number;
|
|
23
23
|
sessionEstimatedRepeatedReadTokens: number;
|
|
24
|
+
orphanedNoteCount: number;
|
|
24
25
|
changes: CognitionRepairChange[];
|
|
25
26
|
}
|
|
26
27
|
export declare function analyzeCognitionQuality(workspace: KGraphWorkspace, maps: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, rename } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { buildSessionReport } from '../session/session-store.js';
|
|
4
|
+
import { overwriteDomainRecord, readCognitionNotes, readDomainRecords, writeCognitionNote, } from '../storage/cognition-store.js';
|
|
3
5
|
export async function analyzeCognitionQuality(workspace, maps) {
|
|
4
6
|
const notes = await readCognitionNotes(workspace);
|
|
5
7
|
const session = await buildSessionReport(workspace);
|
|
@@ -7,6 +9,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
7
9
|
.map((note) => analyzeNote(note, maps))
|
|
8
10
|
.filter((change) => change.removedFileRefs.length > 0 ||
|
|
9
11
|
change.removedSymbolRefs.length > 0);
|
|
12
|
+
const orphanedNoteCount = notes.filter((note) => note.referencesStatus === 'stale').length;
|
|
10
13
|
return {
|
|
11
14
|
noteCount: notes.length,
|
|
12
15
|
mixedOrStaleCount: notes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
|
|
@@ -20,6 +23,7 @@ export async function analyzeCognitionQuality(workspace, maps) {
|
|
|
20
23
|
sessionRepeatedReadCount: session.repeatedReadCount,
|
|
21
24
|
sessionEstimatedReadTokens: session.estimatedReadTokens,
|
|
22
25
|
sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
|
|
26
|
+
orphanedNoteCount,
|
|
23
27
|
changes,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
@@ -40,9 +44,18 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
// Archive fully-orphaned notes (all refs dead) so they no longer appear in context
|
|
48
|
+
const orphanedNotes = nextNotes.filter((note) => note.referencesStatus === 'stale');
|
|
49
|
+
if (!dryRun && (changes.length > 0 || orphanedNotes.length > 0)) {
|
|
50
|
+
// Exclude fully-orphaned notes from domain records — they are being archived
|
|
51
|
+
await repairDomainRecords(workspace, nextNotes.filter((n) => n.referencesStatus !== 'stale'), maps);
|
|
52
|
+
}
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
for (const note of orphanedNotes) {
|
|
55
|
+
await archiveOrphanedNote(workspace, note);
|
|
56
|
+
}
|
|
45
57
|
}
|
|
58
|
+
const orphanedNoteCount = orphanedNotes.length;
|
|
46
59
|
return {
|
|
47
60
|
noteCount: notes.length,
|
|
48
61
|
mixedOrStaleCount: nextNotes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
|
|
@@ -56,6 +69,7 @@ export async function repairCognition(workspace, maps, dryRun = false) {
|
|
|
56
69
|
sessionRepeatedReadCount: session.repeatedReadCount,
|
|
57
70
|
sessionEstimatedReadTokens: session.estimatedReadTokens,
|
|
58
71
|
sessionEstimatedRepeatedReadTokens: session.estimatedRepeatedReadTokens,
|
|
72
|
+
orphanedNoteCount,
|
|
59
73
|
changes,
|
|
60
74
|
};
|
|
61
75
|
}
|
|
@@ -98,7 +112,8 @@ function countGeneratedScannedFiles(fileMap) {
|
|
|
98
112
|
].some((prefix) => file.path === prefix || file.path.startsWith(prefix))).length;
|
|
99
113
|
}
|
|
100
114
|
function countExpensiveFiles(fileMap) {
|
|
101
|
-
return fileMap.files.filter((file) => (file.tokenEstimate ?? 0) >= 1000)
|
|
115
|
+
return fileMap.files.filter((file) => (file.tokenEstimate ?? 0) >= 1000)
|
|
116
|
+
.length;
|
|
102
117
|
}
|
|
103
118
|
function analyzeNote(note, maps) {
|
|
104
119
|
const filePaths = new Set(maps.fileMap.files.map((file) => file.path));
|
|
@@ -161,11 +176,27 @@ function evaluateReferenceStatus(relatedFiles, relatedSymbols, maps) {
|
|
|
161
176
|
return 'mixed';
|
|
162
177
|
}
|
|
163
178
|
function isNoisyFileRef(ref) {
|
|
164
|
-
return !ref.includes('/') && /^[A-Z][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(ref);
|
|
179
|
+
return (!ref.includes('/') && /^[A-Z][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(ref));
|
|
165
180
|
}
|
|
166
181
|
function isNoisySymbolRef(ref) {
|
|
167
|
-
|
|
182
|
+
// Only treat as noise if the ref is a short all-lowercase word (no camelCase, no _ or $).
|
|
183
|
+
// Preserve camelCase refs even when unresolved — the symbol may have been renamed.
|
|
184
|
+
if (/[A-Z_$]/.test(ref))
|
|
185
|
+
return false;
|
|
186
|
+
return ref.length <= 5;
|
|
168
187
|
}
|
|
169
188
|
function unique(items) {
|
|
170
189
|
return [...new Set(items)];
|
|
171
190
|
}
|
|
191
|
+
async function archiveOrphanedNote(workspace, note) {
|
|
192
|
+
const archivedDir = path.join(workspace.cognitionPath, 'archived');
|
|
193
|
+
await mkdir(archivedDir, { recursive: true });
|
|
194
|
+
const source = path.join(workspace.cognitionPath, `${note.id}.md`);
|
|
195
|
+
const target = path.join(archivedDir, `${note.id}.md`);
|
|
196
|
+
try {
|
|
197
|
+
await rename(source, target);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// source file may already be missing — ignore
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -11,7 +11,11 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
|
11
11
|
const raw = await readFile(inboxPath, 'utf8');
|
|
12
12
|
const parsed = parseMarkdownNote(raw);
|
|
13
13
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
14
|
-
|
|
14
|
+
// Always include the inbox basename as a per-note unique suffix so two notes with
|
|
15
|
+
// the same title processed in the same millisecond never receive the same ID.
|
|
16
|
+
const base = path.basename(inboxPath, '.md');
|
|
17
|
+
const slug = slugify(parsed.title);
|
|
18
|
+
const id = slug ? `${timestamp}-${slug}-${base}` : `${timestamp}-${base}`;
|
|
15
19
|
const archivedPath = path.join(workspace.processedInteractionsPath, `${timestamp}-${path.basename(inboxPath)}`);
|
|
16
20
|
const note = {
|
|
17
21
|
...parsed,
|
|
@@ -39,6 +43,11 @@ export async function updateCognition(workspace, currentMaps, dryRun = false) {
|
|
|
39
43
|
warnings.push(`${path.basename(inboxPath)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
// Refresh reference statuses on all existing notes so that notes which
|
|
47
|
+
// became stale since the last scan reflect the current map state.
|
|
48
|
+
if (!dryRun) {
|
|
49
|
+
await refreshCognitionReferenceStatuses(workspace, currentMaps);
|
|
50
|
+
}
|
|
42
51
|
return { processed, warnings };
|
|
43
52
|
}
|
|
44
53
|
export async function refreshCognitionReferenceStatuses(workspace, currentMaps) {
|
|
@@ -14,7 +14,7 @@ export function parseMarkdownNote(markdown) {
|
|
|
14
14
|
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.map(String) : [],
|
|
15
15
|
summary: sections.Summary,
|
|
16
16
|
sections,
|
|
17
|
-
relatedFiles: unique(extractMatches(combined, PATH_REF)),
|
|
17
|
+
relatedFiles: unique(extractMatches(stripCodeFences(combined), PATH_REF)),
|
|
18
18
|
relatedSymbols: unique(extractSymbolRefs(sections)),
|
|
19
19
|
warnings,
|
|
20
20
|
};
|
|
@@ -70,3 +70,8 @@ function extractSymbolRefs(sections) {
|
|
|
70
70
|
function unique(items) {
|
|
71
71
|
return [...new Set(items)];
|
|
72
72
|
}
|
|
73
|
+
function stripCodeFences(text) {
|
|
74
|
+
// Remove triple-backtick code blocks so paths inside code examples are not
|
|
75
|
+
// mistaken for real file references, which would create phantom stale refs.
|
|
76
|
+
return text.replace(/```[\s\S]*?```/g, '');
|
|
77
|
+
}
|
|
@@ -5,10 +5,16 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
5
5
|
const cognition = await readCognitionNotes(workspace);
|
|
6
6
|
const domains = await readDomainRecords(workspace);
|
|
7
7
|
const max = config.maxContextItems;
|
|
8
|
-
|
|
8
|
+
let relevantFiles = rankByFields(query, maps.fileMap.files, [
|
|
9
9
|
{ name: 'path', value: (file) => file.path },
|
|
10
10
|
{ name: 'language', value: (file) => file.language },
|
|
11
|
-
])
|
|
11
|
+
])
|
|
12
|
+
.map((ranked) => ({
|
|
13
|
+
...ranked,
|
|
14
|
+
score: ranked.score - Math.floor((ranked.item.tokenEstimate ?? 0) / 2000),
|
|
15
|
+
}))
|
|
16
|
+
.sort((a, b) => b.score - a.score)
|
|
17
|
+
.slice(0, max);
|
|
12
18
|
const relevantSymbols = rankByFields(query, maps.symbolMap.symbols, [
|
|
13
19
|
{ name: 'name', value: (symbol) => symbol.name },
|
|
14
20
|
{ name: 'path', value: (symbol) => symbol.filePath },
|
|
@@ -28,6 +34,57 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
28
34
|
{ name: 'tags', value: (domain) => domain.tags },
|
|
29
35
|
{ name: 'path', value: (domain) => domain.pathHints },
|
|
30
36
|
]).slice(0, max);
|
|
37
|
+
// Inject files linked by matched cognition notes/domains that didn't score on name alone
|
|
38
|
+
const rankedFilePaths = new Set(relevantFiles.map((f) => f.item.path));
|
|
39
|
+
const cognitionLinkedMap = new Map();
|
|
40
|
+
for (const ranked of relevantCognition) {
|
|
41
|
+
for (const fp of ranked.item.relatedFiles) {
|
|
42
|
+
if (!rankedFilePaths.has(fp)) {
|
|
43
|
+
const reasons = cognitionLinkedMap.get(fp) ?? [];
|
|
44
|
+
reasons.push(`linked by cognition note "${ranked.item.title}"`);
|
|
45
|
+
cognitionLinkedMap.set(fp, reasons);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const ranked of matchedDomains) {
|
|
50
|
+
for (const fp of ranked.item.files) {
|
|
51
|
+
if (!rankedFilePaths.has(fp)) {
|
|
52
|
+
const reasons = cognitionLinkedMap.get(fp) ?? [];
|
|
53
|
+
reasons.push(`in domain "${ranked.item.name}"`);
|
|
54
|
+
cognitionLinkedMap.set(fp, reasons);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Apply domainHints from config: inject paths for hints whose name matches the query
|
|
59
|
+
const queryTokens = new Set(query
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.split(/[^a-z0-9]+/)
|
|
62
|
+
.filter(Boolean));
|
|
63
|
+
for (const [hintName, hint] of Object.entries(config.domainHints)) {
|
|
64
|
+
const hintWords = hintName
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.split(/[^a-z0-9]+/)
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
if (!hintWords.some((w) => queryTokens.has(w)))
|
|
69
|
+
continue;
|
|
70
|
+
for (const fp of hint.paths ?? []) {
|
|
71
|
+
if (!rankedFilePaths.has(fp)) {
|
|
72
|
+
const reasons = cognitionLinkedMap.get(fp) ?? [];
|
|
73
|
+
reasons.push(`in configured domain hint "${hintName}"`);
|
|
74
|
+
cognitionLinkedMap.set(fp, reasons);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
relevantFiles = [
|
|
79
|
+
...relevantFiles,
|
|
80
|
+
...maps.fileMap.files
|
|
81
|
+
.filter((f) => cognitionLinkedMap.has(f.path))
|
|
82
|
+
.map((f) => ({
|
|
83
|
+
item: f,
|
|
84
|
+
score: 1,
|
|
85
|
+
reasons: cognitionLinkedMap.get(f.path),
|
|
86
|
+
})),
|
|
87
|
+
];
|
|
31
88
|
const relatedIds = new Set([
|
|
32
89
|
...relevantFiles.map((file) => file.item.path),
|
|
33
90
|
...relevantSymbols.map((symbol) => symbol.item.id),
|
|
@@ -69,10 +126,12 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
69
126
|
});
|
|
70
127
|
const filePaths = new Set(maps.fileMap.files.map((f) => f.path));
|
|
71
128
|
const symbolNames = new Set(maps.symbolMap.symbols.map((s) => s.name));
|
|
129
|
+
const matchedCognitionIds = new Set(relevantCognition.map((r) => r.item.id));
|
|
72
130
|
const staleReferences = cognition
|
|
73
|
-
.filter((note) => note.
|
|
74
|
-
note.referencesStatus === '
|
|
75
|
-
|
|
131
|
+
.filter((note) => matchedCognitionIds.has(note.id) &&
|
|
132
|
+
(note.referencesStatus === 'stale' ||
|
|
133
|
+
note.referencesStatus === 'unresolved' ||
|
|
134
|
+
note.referencesStatus === 'mixed'))
|
|
76
135
|
.flatMap((note) => [
|
|
77
136
|
...note.relatedFiles
|
|
78
137
|
.filter((f) => !filePaths.has(f))
|
|
@@ -98,9 +157,18 @@ export async function queryContext(workspace, config, maps, query) {
|
|
|
98
157
|
// Remove files already in the matched set
|
|
99
158
|
for (const p of matchedFilePaths)
|
|
100
159
|
importedFilePaths.delete(p);
|
|
160
|
+
// Skip generic utility/barrel files with many exports — surface only focused modules
|
|
161
|
+
const exportCountByFile = new Map();
|
|
162
|
+
for (const s of maps.symbolMap.symbols) {
|
|
163
|
+
if (s.exported) {
|
|
164
|
+
exportCountByFile.set(s.filePath, (exportCountByFile.get(s.filePath) ?? 0) + 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const MAX_NEARBY_FILE_EXPORTS = 15;
|
|
168
|
+
const relevantImportedFilePaths = new Set([...importedFilePaths].filter((fp) => (exportCountByFile.get(fp) ?? 0) <= MAX_NEARBY_FILE_EXPORTS));
|
|
101
169
|
const nearbySymbols = maps.symbolMap.symbols
|
|
102
170
|
.filter((s) => s.exported &&
|
|
103
|
-
|
|
171
|
+
relevantImportedFilePaths.has(s.filePath) &&
|
|
104
172
|
!matchedSymbolIds.has(s.id))
|
|
105
173
|
.slice(0, max);
|
|
106
174
|
const nearbySymbolExplanations = nearbySymbols.map((symbol) => ({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { KGraphError } from '../cli/errors.js';
|
|
3
4
|
import { getIntegrationAdapter } from '../integrations/integration-registry.js';
|
|
4
5
|
import { pathExists } from '../storage/kgraph-paths.js';
|
|
5
|
-
import { KGraphError } from '../cli/errors.js';
|
|
6
6
|
import { estimateTokens } from './token-estimator.js';
|
|
7
7
|
const EMPTY_STATE = {
|
|
8
8
|
active: {},
|
|
@@ -32,6 +32,12 @@ export async function readSessionLedger(workspace) {
|
|
|
32
32
|
export async function recordSessionEvent(workspace, input) {
|
|
33
33
|
const now = new Date().toISOString();
|
|
34
34
|
const state = await readSessionState(workspace);
|
|
35
|
+
// Auto-close any open session for this agent before starting a new one so
|
|
36
|
+
// the ledger entry is never silently lost on repeated start calls.
|
|
37
|
+
if (input.type === 'start' && state.active[input.agent]) {
|
|
38
|
+
await appendLedgerEntry(workspace, summarizeAgentSession(input.agent, state, now));
|
|
39
|
+
delete state.active[input.agent];
|
|
40
|
+
}
|
|
35
41
|
const active = state.active[input.agent] ?? {
|
|
36
42
|
agent: input.agent,
|
|
37
43
|
sessionId: `${input.agent}-${now.replace(/[:.]/g, '-')}`,
|
|
@@ -146,7 +152,11 @@ function topRepeatedReads(events) {
|
|
|
146
152
|
for (const event of events) {
|
|
147
153
|
if (!event.path)
|
|
148
154
|
continue;
|
|
149
|
-
const current = byPath.get(event.path) ?? {
|
|
155
|
+
const current = byPath.get(event.path) ?? {
|
|
156
|
+
path: event.path,
|
|
157
|
+
count: 0,
|
|
158
|
+
estimatedTokens: 0,
|
|
159
|
+
};
|
|
150
160
|
current.count += 1;
|
|
151
161
|
current.estimatedTokens += event.tokenEstimate ?? 0;
|
|
152
162
|
byPath.set(event.path, current);
|