aicp-tracker 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/log-parser.js +164 -71
- package/src/state.js +22 -1
package/package.json
CHANGED
package/src/log-parser.js
CHANGED
|
@@ -25,11 +25,9 @@ function sumUsage(entries) {
|
|
|
25
25
|
cc += u.cache_creation_input_tokens || 0;
|
|
26
26
|
cr += u.cache_read_input_tokens || 0;
|
|
27
27
|
out += u.output_tokens || 0;
|
|
28
|
-
// Ephemeral tokens may be at top level (older format) or in cache_creation object (newer format)
|
|
29
28
|
ephUser += u.ephemeral_user_input_tokens || u.cache_creation?.ephemeral_user_input_tokens || 0;
|
|
30
29
|
ephAsst += u.ephemeral_assistant_input_tokens || u.cache_creation?.ephemeral_assistant_input_tokens || 0;
|
|
31
30
|
ephTool += u.ephemeral_tool_input_tokens || u.cache_creation?.ephemeral_tool_input_tokens || 0;
|
|
32
|
-
// Also sum ephemeral cache creation tokens (5m and 1h variants)
|
|
33
31
|
ephUser += u.cache_creation?.ephemeral_5m_input_tokens || 0;
|
|
34
32
|
ephUser += u.cache_creation?.ephemeral_1h_input_tokens || 0;
|
|
35
33
|
if (!serviceTier && u.service_tier) serviceTier = u.service_tier;
|
|
@@ -43,7 +41,6 @@ function sumUsage(entries) {
|
|
|
43
41
|
if (block.name === 'web_fetch' || block.name === 'WebFetch') webFetch++;
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
|
-
// Also check server_tool_use object for web requests
|
|
47
44
|
const srvTool = u.server_tool_use;
|
|
48
45
|
if (srvTool) {
|
|
49
46
|
webSearch += srvTool.web_search_requests || 0;
|
|
@@ -55,6 +52,59 @@ function sumUsage(entries) {
|
|
|
55
52
|
return { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models, uuids };
|
|
56
53
|
}
|
|
57
54
|
|
|
55
|
+
// Merge two usage objects (both have models as Set, uuids as array)
|
|
56
|
+
function mergeUsages(a, b) {
|
|
57
|
+
return {
|
|
58
|
+
input: a.input + b.input,
|
|
59
|
+
cc: a.cc + b.cc,
|
|
60
|
+
cr: a.cr + b.cr,
|
|
61
|
+
out: a.out + b.out,
|
|
62
|
+
ephUser: a.ephUser + b.ephUser,
|
|
63
|
+
ephAsst: a.ephAsst + b.ephAsst,
|
|
64
|
+
ephTool: a.ephTool + b.ephTool,
|
|
65
|
+
webSearch: a.webSearch + b.webSearch,
|
|
66
|
+
webFetch: a.webFetch + b.webFetch,
|
|
67
|
+
serviceTier: a.serviceTier || b.serviceTier,
|
|
68
|
+
speed: a.speed || b.speed,
|
|
69
|
+
models: new Set([...a.models, ...b.models]),
|
|
70
|
+
uuids: [...a.uuids, ...b.uuids],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Convert usage (with Set) to JSON-serializable form for state storage
|
|
75
|
+
function usageToPlain(u) {
|
|
76
|
+
return {
|
|
77
|
+
input: u.input, cc: u.cc, cr: u.cr, out: u.out,
|
|
78
|
+
ephUser: u.ephUser, ephAsst: u.ephAsst, ephTool: u.ephTool,
|
|
79
|
+
webSearch: u.webSearch, webFetch: u.webFetch,
|
|
80
|
+
serviceTier: u.serviceTier, speed: u.speed,
|
|
81
|
+
models: [...u.models],
|
|
82
|
+
uuids: u.uuids,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Restore usage from plain state (convert models array back to Set)
|
|
87
|
+
function usageFromPlain(p) {
|
|
88
|
+
return {
|
|
89
|
+
input: p.input || 0, cc: p.cc || 0, cr: p.cr || 0, out: p.out || 0,
|
|
90
|
+
ephUser: p.ephUser || 0, ephAsst: p.ephAsst || 0, ephTool: p.ephTool || 0,
|
|
91
|
+
webSearch: p.webSearch || 0, webFetch: p.webFetch || 0,
|
|
92
|
+
serviceTier: p.serviceTier || null, speed: p.speed || null,
|
|
93
|
+
models: new Set(p.models || []),
|
|
94
|
+
uuids: p.uuids || [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract the minimal ref fields needed by makeRecord from any JSONL entry
|
|
99
|
+
function refFromEntry(entry) {
|
|
100
|
+
return {
|
|
101
|
+
sessionId: entry.sessionId || null,
|
|
102
|
+
timestamp: entry.timestamp || null,
|
|
103
|
+
gitBranch: entry.gitBranch || null,
|
|
104
|
+
parentUuid: entry.parentUuid || null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
function makeRecord(ref, rootUuid, usage, filePaths) {
|
|
59
109
|
const { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models, uuids } = usage;
|
|
60
110
|
return {
|
|
@@ -107,13 +157,69 @@ function extractAllFilePaths(entry) {
|
|
|
107
157
|
return paths;
|
|
108
158
|
}
|
|
109
159
|
|
|
160
|
+
function extractGroupFilePaths(allEntries) {
|
|
161
|
+
const paths = [];
|
|
162
|
+
for (const entry of allEntries) {
|
|
163
|
+
for (const fp of extractAllFilePaths(entry)) paths.push(fp);
|
|
164
|
+
}
|
|
165
|
+
return [...new Set(paths)];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Emit a closed group with per-file splitting (used for groups fully contained in one tick)
|
|
169
|
+
function emitGroup(ref, baseUuid, turns, allEntries) {
|
|
170
|
+
const allFiles = new Set();
|
|
171
|
+
for (const entry of allEntries) {
|
|
172
|
+
for (const fp of extractAllFilePaths(entry)) allFiles.add(fp);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!allFiles.size) {
|
|
176
|
+
return [makeRecord(ref, baseUuid, sumUsage(turns), [])];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const turnFiles = turns.map(extractFilePath);
|
|
180
|
+
const fileGroups = new Map();
|
|
181
|
+
const firstAnchorIdx = turnFiles.findIndex(f => f !== null);
|
|
182
|
+
let lastFile = turnFiles[firstAnchorIdx];
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < turns.length; i++) {
|
|
185
|
+
const file = turnFiles[i] !== null ? turnFiles[i]
|
|
186
|
+
: (i < firstAnchorIdx ? turnFiles[firstAnchorIdx] : lastFile);
|
|
187
|
+
if (!fileGroups.has(file)) fileGroups.set(file, []);
|
|
188
|
+
fileGroups.get(file).push(turns[i]);
|
|
189
|
+
if (turnFiles[i] !== null) lastFile = turnFiles[i];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const records = [];
|
|
193
|
+
let idx = 0;
|
|
194
|
+
for (const [fp, fileTurns] of fileGroups) {
|
|
195
|
+
const uuid = idx === 0 ? baseUuid : `${baseUuid}:${idx}`;
|
|
196
|
+
const filesForRecord = [...new Set([fp, ...fileTurns.flatMap(extractAllFilePaths)])];
|
|
197
|
+
records.push(makeRecord(ref, uuid, sumUsage(fileTurns), filesForRecord));
|
|
198
|
+
idx++;
|
|
199
|
+
}
|
|
200
|
+
return records;
|
|
201
|
+
}
|
|
202
|
+
|
|
110
203
|
function parseNewLines(filePath) {
|
|
111
204
|
const carryOverPromptId = state.getLastPromptId(filePath);
|
|
205
|
+
const pendingGroup = state.getPendingGroup(filePath);
|
|
112
206
|
|
|
113
207
|
const offset = state.getOffset(filePath);
|
|
114
208
|
let stat;
|
|
115
209
|
try { stat = fs.statSync(filePath); } catch { return []; }
|
|
116
|
-
|
|
210
|
+
|
|
211
|
+
// No new bytes — flush any pending group now (the session is done or paused long enough)
|
|
212
|
+
if (stat.size <= offset) {
|
|
213
|
+
if (pendingGroup) {
|
|
214
|
+
const usage = usageFromPlain(pendingGroup.usage);
|
|
215
|
+
if (usage.uuids.length > 0) {
|
|
216
|
+
state.clearPendingGroup(filePath);
|
|
217
|
+
return [makeRecord(pendingGroup.ref, pendingGroup.promptId, usage, pendingGroup.filePaths || [])];
|
|
218
|
+
}
|
|
219
|
+
state.clearPendingGroup(filePath);
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
117
223
|
|
|
118
224
|
const fd = fs.openSync(filePath, 'r');
|
|
119
225
|
const buf = Buffer.alloc(stat.size - offset);
|
|
@@ -121,11 +227,8 @@ function parseNewLines(filePath) {
|
|
|
121
227
|
fs.closeSync(fd);
|
|
122
228
|
state.setOffset(filePath, offset + read);
|
|
123
229
|
|
|
124
|
-
// Parse
|
|
125
|
-
// usage
|
|
126
|
-
// identical and would cause token double-counting. Entries without usage
|
|
127
|
-
// (pure tool-call lines, tool-result lines, etc.) are all kept so that no
|
|
128
|
-
// file-path information is accidentally discarded.
|
|
230
|
+
// Parse entries; deduplicate by message.id only for entries that carry usage data
|
|
231
|
+
// (non-usage entries like tool-call lines must all be kept to avoid losing file paths)
|
|
129
232
|
const entries = [];
|
|
130
233
|
const seenUsageMsgIds = new Set();
|
|
131
234
|
|
|
@@ -146,23 +249,14 @@ function parseNewLines(filePath) {
|
|
|
146
249
|
|
|
147
250
|
// ── Sequential grouping ───────────────────────────────────────────────────
|
|
148
251
|
// Any entry that carries a promptId opens (or re-opens) a group.
|
|
149
|
-
// Every subsequent entry
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// you find a new promptId."
|
|
154
|
-
//
|
|
155
|
-
// carryOverPromptId: the last promptId seen in a previous batch so that
|
|
156
|
-
// assistant entries at the very start of this batch (before any new
|
|
157
|
-
// promptId) are still collected rather than dropped.
|
|
158
|
-
|
|
159
|
-
let currentPromptId = carryOverPromptId;
|
|
160
|
-
let latestPromptId = carryOverPromptId;
|
|
161
|
-
let carryOverSeenInBatch = false;
|
|
252
|
+
// Every subsequent entry belongs to that group until the next promptId appears.
|
|
253
|
+
|
|
254
|
+
let currentPromptId = carryOverPromptId;
|
|
255
|
+
let latestPromptId = carryOverPromptId;
|
|
162
256
|
|
|
163
257
|
const groupOrder = [];
|
|
164
|
-
const groupAllEntries = new Map();
|
|
165
|
-
const groupRef = new Map();
|
|
258
|
+
const groupAllEntries = new Map();
|
|
259
|
+
const groupRef = new Map();
|
|
166
260
|
|
|
167
261
|
function ensureGroup(pid, refEntry) {
|
|
168
262
|
if (!groupAllEntries.has(pid)) {
|
|
@@ -174,13 +268,10 @@ function parseNewLines(filePath) {
|
|
|
174
268
|
|
|
175
269
|
for (const entry of entries) {
|
|
176
270
|
if (entry.promptId) {
|
|
177
|
-
// This entry opens a new group (could be any role/type)
|
|
178
|
-
if (entry.promptId === carryOverPromptId) carryOverSeenInBatch = true;
|
|
179
271
|
currentPromptId = entry.promptId;
|
|
180
272
|
latestPromptId = entry.promptId;
|
|
181
273
|
ensureGroup(currentPromptId, entry);
|
|
182
274
|
} else if (currentPromptId) {
|
|
183
|
-
// No promptId → belongs to whatever group is currently open
|
|
184
275
|
ensureGroup(currentPromptId, entry);
|
|
185
276
|
groupAllEntries.get(currentPromptId).push(entry);
|
|
186
277
|
}
|
|
@@ -191,54 +282,56 @@ function parseNewLines(filePath) {
|
|
|
191
282
|
// ── Emit records ──────────────────────────────────────────────────────────
|
|
192
283
|
const records = [];
|
|
193
284
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
const ref = groupRef.get(promptId);
|
|
201
|
-
|
|
202
|
-
// Suffix :carry when this is a pure continuation from a previous batch so
|
|
203
|
-
// both records land in the DB rather than one being deduplicated away.
|
|
204
|
-
const isCarryOver = promptId === carryOverPromptId && !carryOverSeenInBatch;
|
|
205
|
-
const baseUuid = isCarryOver ? `${promptId}:carry` : promptId;
|
|
206
|
-
|
|
207
|
-
// Collect file paths from EVERY entry in the group (tool-use lines,
|
|
208
|
-
// tool-result lines, etc.) not just the ones that carry usage.
|
|
209
|
-
const allFiles = new Set();
|
|
210
|
-
for (const entry of allEntries) {
|
|
211
|
-
for (const fp of extractAllFilePaths(entry)) {
|
|
212
|
-
allFiles.add(fp);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (!allFiles.size) {
|
|
217
|
-
records.push(makeRecord(ref, baseUuid, sumUsage(turns), []));
|
|
218
|
-
continue;
|
|
285
|
+
// If a pending group exists but its promptId doesn't appear in this batch at all,
|
|
286
|
+
// it has been superseded by new prompts — emit it now before processing new groups.
|
|
287
|
+
if (pendingGroup && groupOrder.length > 0 && !groupOrder.includes(pendingGroup.promptId)) {
|
|
288
|
+
const usage = usageFromPlain(pendingGroup.usage);
|
|
289
|
+
if (usage.uuids.length > 0) {
|
|
290
|
+
records.push(makeRecord(pendingGroup.ref, pendingGroup.promptId, usage, pendingGroup.filePaths || []));
|
|
219
291
|
}
|
|
292
|
+
state.clearPendingGroup(filePath);
|
|
293
|
+
}
|
|
220
294
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
295
|
+
for (let gi = 0; gi < groupOrder.length; gi++) {
|
|
296
|
+
const promptId = groupOrder[gi];
|
|
297
|
+
const isLast = gi === groupOrder.length - 1;
|
|
298
|
+
const allEntries = groupAllEntries.get(promptId);
|
|
299
|
+
const turns = allEntries.filter(e => e.message?.usage);
|
|
300
|
+
const hasPending = pendingGroup && pendingGroup.promptId === promptId;
|
|
301
|
+
|
|
302
|
+
let effectiveUsage, effectiveFilePaths, effectiveRef;
|
|
303
|
+
|
|
304
|
+
if (hasPending) {
|
|
305
|
+
// Merge accumulated pending data with new entries from this tick
|
|
306
|
+
const pendingUsage = usageFromPlain(pendingGroup.usage);
|
|
307
|
+
effectiveUsage = turns.length > 0 ? mergeUsages(pendingUsage, sumUsage(turns)) : pendingUsage;
|
|
308
|
+
effectiveFilePaths = [...new Set([...(pendingGroup.filePaths || []), ...extractGroupFilePaths(allEntries)])];
|
|
309
|
+
effectiveRef = pendingGroup.ref;
|
|
310
|
+
} else {
|
|
311
|
+
if (!turns.length) continue;
|
|
312
|
+
effectiveUsage = sumUsage(turns);
|
|
313
|
+
effectiveFilePaths = extractGroupFilePaths(allEntries);
|
|
314
|
+
effectiveRef = refFromEntry(groupRef.get(promptId));
|
|
234
315
|
}
|
|
235
316
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
317
|
+
if (isLast) {
|
|
318
|
+
// Group may still grow in the next tick — save as pending, don't emit yet
|
|
319
|
+
state.setPendingGroup(filePath, {
|
|
320
|
+
promptId,
|
|
321
|
+
ref: effectiveRef,
|
|
322
|
+
usage: usageToPlain(effectiveUsage),
|
|
323
|
+
filePaths: effectiveFilePaths,
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
// Group is closed (a subsequent promptId appeared) — emit now
|
|
327
|
+
if (hasPending) state.clearPendingGroup(filePath);
|
|
328
|
+
if (hasPending) {
|
|
329
|
+
// Cross-tick group: emit as one record (tokens already merged, no per-file split)
|
|
330
|
+
records.push(makeRecord(effectiveRef, promptId, effectiveUsage, effectiveFilePaths));
|
|
331
|
+
} else {
|
|
332
|
+
// Single-tick group: use per-file splitting as before
|
|
333
|
+
records.push(...emitGroup(effectiveRef, promptId, turns, allEntries));
|
|
334
|
+
}
|
|
242
335
|
}
|
|
243
336
|
}
|
|
244
337
|
|
package/src/state.js
CHANGED
|
@@ -44,4 +44,25 @@ function setLastPromptId(filePath, promptId) {
|
|
|
44
44
|
save();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Pending group: the last open group that has not yet been closed by a subsequent promptId.
|
|
48
|
+
// Stored until the group is definitively closed (different promptId appears, or file stops growing).
|
|
49
|
+
function getPendingGroup(filePath) {
|
|
50
|
+
return load().pendingGroups?.[filePath] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setPendingGroup(filePath, data) {
|
|
54
|
+
const s = load();
|
|
55
|
+
if (!s.pendingGroups) s.pendingGroups = {};
|
|
56
|
+
s.pendingGroups[filePath] = data;
|
|
57
|
+
save();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clearPendingGroup(filePath) {
|
|
61
|
+
const s = load();
|
|
62
|
+
if (s.pendingGroups?.[filePath]) {
|
|
63
|
+
delete s.pendingGroups[filePath];
|
|
64
|
+
save();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { getOffset, setOffset, getLastPromptId, setLastPromptId, getPendingGroup, setPendingGroup, clearPendingGroup };
|