aicp-tracker 1.2.9 → 1.3.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/package.json +1 -1
- package/src/log-parser.js +36 -42
package/package.json
CHANGED
package/src/log-parser.js
CHANGED
|
@@ -121,11 +121,13 @@ function parseNewLines(filePath) {
|
|
|
121
121
|
fs.closeSync(fd);
|
|
122
122
|
state.setOffset(filePath, offset + read);
|
|
123
123
|
|
|
124
|
-
// Parse entries
|
|
125
|
-
//
|
|
126
|
-
//
|
|
124
|
+
// Parse all entries. Only deduplicate by message.id for entries that carry
|
|
125
|
+
// usage data — those are the only ones where duplicate lines are truly
|
|
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.
|
|
127
129
|
const entries = [];
|
|
128
|
-
const
|
|
130
|
+
const seenUsageMsgIds = new Set();
|
|
129
131
|
|
|
130
132
|
for (const raw of buf.slice(0, read).toString('utf8').split('\n')) {
|
|
131
133
|
const line = raw.trim();
|
|
@@ -135,36 +137,35 @@ function parseNewLines(filePath) {
|
|
|
135
137
|
if (!entry.uuid) continue;
|
|
136
138
|
|
|
137
139
|
const msgId = entry.message?.id;
|
|
138
|
-
if (msgId) {
|
|
139
|
-
if (
|
|
140
|
-
|
|
140
|
+
if (msgId && entry.message?.usage) {
|
|
141
|
+
if (seenUsageMsgIds.has(msgId)) continue;
|
|
142
|
+
seenUsageMsgIds.add(msgId);
|
|
141
143
|
}
|
|
142
144
|
entries.push(entry);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
// ── Sequential grouping ───────────────────────────────────────────────────
|
|
146
|
-
//
|
|
147
|
-
// Every
|
|
148
|
-
//
|
|
149
|
-
//
|
|
148
|
+
// Any entry that carries a promptId opens (or re-opens) a group.
|
|
149
|
+
// Every subsequent entry — regardless of type or role — belongs to that
|
|
150
|
+
// group until the next promptId appears. This is the approach the user
|
|
151
|
+
// requested: "start from the record that has a promptId and go through all
|
|
152
|
+
// records down, counting them in, even if they don't have a promptId, until
|
|
153
|
+
// you find a new promptId."
|
|
150
154
|
//
|
|
151
|
-
// carryOverPromptId: the last promptId seen in a previous batch
|
|
152
|
-
// entries at the start of this batch
|
|
153
|
-
//
|
|
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.
|
|
154
158
|
|
|
155
|
-
let currentPromptId
|
|
156
|
-
let latestPromptId
|
|
159
|
+
let currentPromptId = carryOverPromptId;
|
|
160
|
+
let latestPromptId = carryOverPromptId;
|
|
157
161
|
let carryOverSeenInBatch = false;
|
|
158
162
|
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const groupAllEntries = new Map(); // promptId → ALL entries in this turn (for file extraction)
|
|
163
|
-
const groupRef = new Map(); // promptId → reference entry (user msg or first asst turn)
|
|
163
|
+
const groupOrder = [];
|
|
164
|
+
const groupAllEntries = new Map(); // promptId → every non-prompt entry in the turn
|
|
165
|
+
const groupRef = new Map(); // promptId → the entry that opened the group
|
|
164
166
|
|
|
165
167
|
function ensureGroup(pid, refEntry) {
|
|
166
|
-
if (!
|
|
167
|
-
groupTurns.set(pid, []);
|
|
168
|
+
if (!groupAllEntries.has(pid)) {
|
|
168
169
|
groupAllEntries.set(pid, []);
|
|
169
170
|
groupRef.set(pid, refEntry);
|
|
170
171
|
groupOrder.push(pid);
|
|
@@ -172,21 +173,16 @@ function parseNewLines(filePath) {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
for (const entry of entries) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (isUser && entry.promptId) {
|
|
176
|
+
if (entry.promptId) {
|
|
177
|
+
// This entry opens a new group (could be any role/type)
|
|
179
178
|
if (entry.promptId === carryOverPromptId) carryOverSeenInBatch = true;
|
|
180
179
|
currentPromptId = entry.promptId;
|
|
181
180
|
latestPromptId = entry.promptId;
|
|
182
181
|
ensureGroup(currentPromptId, entry);
|
|
183
|
-
} else if (
|
|
184
|
-
//
|
|
182
|
+
} else if (currentPromptId) {
|
|
183
|
+
// No promptId → belongs to whatever group is currently open
|
|
185
184
|
ensureGroup(currentPromptId, entry);
|
|
186
185
|
groupAllEntries.get(currentPromptId).push(entry);
|
|
187
|
-
if (entry.message?.usage) {
|
|
188
|
-
groupTurns.get(currentPromptId).push(entry);
|
|
189
|
-
}
|
|
190
186
|
}
|
|
191
187
|
}
|
|
192
188
|
|
|
@@ -196,36 +192,35 @@ function parseNewLines(filePath) {
|
|
|
196
192
|
const records = [];
|
|
197
193
|
|
|
198
194
|
for (const promptId of groupOrder) {
|
|
199
|
-
const turns = groupTurns.get(promptId);
|
|
200
195
|
const allEntries = groupAllEntries.get(promptId);
|
|
196
|
+
// Only entries that actually carry usage data contribute to token counts
|
|
197
|
+
const turns = allEntries.filter(e => e.message?.usage);
|
|
201
198
|
if (!turns.length) continue;
|
|
202
199
|
|
|
203
200
|
const ref = groupRef.get(promptId);
|
|
204
201
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// the DB rather than one being deduplicated away.
|
|
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.
|
|
208
204
|
const isCarryOver = promptId === carryOverPromptId && !carryOverSeenInBatch;
|
|
209
205
|
const baseUuid = isCarryOver ? `${promptId}:carry` : promptId;
|
|
210
206
|
|
|
211
|
-
// Collect
|
|
212
|
-
// not just
|
|
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.
|
|
213
209
|
const allFiles = new Set();
|
|
214
|
-
const turnFiles = turns.map(extractFilePath);
|
|
215
210
|
for (const entry of allEntries) {
|
|
216
211
|
for (const fp of extractAllFilePaths(entry)) {
|
|
217
212
|
allFiles.add(fp);
|
|
218
213
|
}
|
|
219
214
|
}
|
|
220
|
-
const hasAnyFile = allFiles.size > 0;
|
|
221
215
|
|
|
222
|
-
if (!
|
|
216
|
+
if (!allFiles.size) {
|
|
223
217
|
records.push(makeRecord(ref, baseUuid, sumUsage(turns), []));
|
|
224
218
|
continue;
|
|
225
219
|
}
|
|
226
220
|
|
|
227
221
|
// Attribute orphan turns (no file anchor) to the nearest anchor:
|
|
228
222
|
// leading orphans → first anchor; trailing orphans → last anchor seen.
|
|
223
|
+
const turnFiles = turns.map(extractFilePath);
|
|
229
224
|
const fileGroups = new Map();
|
|
230
225
|
const firstAnchorIdx = turnFiles.findIndex(f => f !== null);
|
|
231
226
|
let lastFile = turnFiles[firstAnchorIdx];
|
|
@@ -241,7 +236,6 @@ function parseNewLines(filePath) {
|
|
|
241
236
|
let idx = 0;
|
|
242
237
|
for (const [fp, fileTurns] of fileGroups) {
|
|
243
238
|
const uuid = idx === 0 ? baseUuid : `${baseUuid}:${idx}`;
|
|
244
|
-
// Include all files from the turn(s) in this group, not just the grouped file
|
|
245
239
|
const filesForRecord = [...new Set([fp, ...fileTurns.flatMap(extractAllFilePaths)])];
|
|
246
240
|
records.push(makeRecord(ref, uuid, sumUsage(fileTurns), filesForRecord));
|
|
247
241
|
idx++;
|