tokentracker-cli 0.18.1 → 0.20.0
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/README.md +5 -4
- package/README.zh-CN.md +4 -3
- package/dashboard/dist/assets/{Card-8ZPdKuRR.js → Card-jA08WeEw.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-OVP6u_7i.js → DashboardPage-chDVOYmG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BxnaPv7O.js → FadeIn-DqSYXuUL.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-8z6DrTLD.js → HeaderGithubStar-C11rWv0B.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-yagKgpi7.js → IpCheckPage-CkEZ9yLK.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Ca72J5F0.js → LandingPage-BgckTHRQ.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CSmW4lBz.js → LeaderboardPage-BCNW7UWp.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-BOihURRE.js → LeaderboardProfilePage-BLATxMt-.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-Bq4zB2w9.js → LimitsPage-arF--WgR.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CoB1ZkE6.js → LoginPage-DpoFP0va.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-D7d5-v70.js → PopoverPopup-kdgc2H6C.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DzvUcjPu.js → ProviderIcon-DV5r9qqP.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BeyW1iTj.js → SettingsPage-Bb22ORmU.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-B6auz1NO.js → SkillsPage-xhtBqVKC.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-C9t8qw0F.js → WidgetsPage-CUoSVDET.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C8RgL-uJ.js → chevron-down-DYb2EChD.js} +1 -1
- package/dashboard/dist/assets/{download-C90EEqc8.js → download-C-_8o6dh.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-BgqTAms5.js → leaderboard-columns-BgzBlYo7.js} +1 -1
- package/dashboard/dist/assets/{main-DJcfmlDf.js → main-11hApDak.js} +6 -4
- package/dashboard/dist/assets/{use-limits-display-prefs-BUBBOUIF.js → use-limits-display-prefs-BeGKWUuk.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CFUEzyoi.js → use-native-settings-nTTHktn0.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-NZDZrVKK.js → use-reduced-motion-DU8Gm6j1.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CoOOhZrW.js → use-usage-limits-DTPmEB8Y.js} +1 -1
- package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
- package/dashboard/dist/brand-logos/every-code.svg +1 -0
- package/dashboard/dist/brand-logos/grok.svg +1 -0
- package/dashboard/dist/brand-logos/hermes.svg +1 -11
- package/dashboard/dist/brand-logos/kilo-code.svg +1 -0
- package/dashboard/dist/brand-logos/oh-my-pi.svg +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +9 -5
- package/src/commands/serve.js +0 -12
- package/src/commands/sync.js +370 -7
- package/src/lib/grok-hook.js +86 -7
- package/src/lib/pricing/curated-overrides.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +438 -148
- package/src/lib/subscriptions.js +92 -40
- package/src/lib/usage-limits.js +1 -1
package/src/lib/rollout.js
CHANGED
|
@@ -3005,18 +3005,61 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
3005
3005
|
// Hermes Agent — SQLite-based (sessions table in ~/.hermes/state.db)
|
|
3006
3006
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3007
3007
|
|
|
3008
|
-
function
|
|
3008
|
+
function resolveHermesPath(env = process.env) {
|
|
3009
|
+
const override = env.TOKENTRACKER_HERMES_HOME;
|
|
3010
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
3011
|
+
return override.trim();
|
|
3012
|
+
}
|
|
3009
3013
|
const home = require("node:os").homedir();
|
|
3010
|
-
return path.join(home, ".hermes"
|
|
3014
|
+
return path.join(home, ".hermes");
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function resolveHermesDbPath(env = process.env) {
|
|
3018
|
+
return path.join(resolveHermesPath(env), "state.db");
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function resolveAllHermesDBPaths({ hermesPath, dbPath } = {}) {
|
|
3022
|
+
const hermesDir = hermesPath ?? (dbPath ? path.dirname(dbPath) : resolveHermesPath());
|
|
3023
|
+
const defaultDbPath = dbPath ?? path.join(hermesDir, "state.db");
|
|
3024
|
+
const profilePaths = {};
|
|
3025
|
+
try {
|
|
3026
|
+
const profilesDir = path.join(hermesDir, "profiles");
|
|
3027
|
+
const profiles = fssync.readdirSync(profilesDir, { withFileTypes: true })
|
|
3028
|
+
.filter((entry) => entry.isDirectory())
|
|
3029
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3030
|
+
|
|
3031
|
+
for (const entry of profiles) {
|
|
3032
|
+
const dbPath = path.join(profilesDir, entry.name, "state.db");
|
|
3033
|
+
if (fssync.existsSync(dbPath)) {
|
|
3034
|
+
profilePaths[entry.name] = dbPath;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
} catch (_e) { }
|
|
3038
|
+
|
|
3039
|
+
return {
|
|
3040
|
+
default: fssync.existsSync(defaultDbPath) ? defaultDbPath : null,
|
|
3041
|
+
profiles: profilePaths,
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
function sqliteStringLiteral(value) {
|
|
3046
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
3011
3047
|
}
|
|
3012
3048
|
|
|
3013
|
-
function readHermesSessions(dbPath, lastCompletedEpoch) {
|
|
3049
|
+
function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = []) {
|
|
3014
3050
|
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
3015
3051
|
const since = Number.isFinite(lastCompletedEpoch) && lastCompletedEpoch > 0 ? lastCompletedEpoch : 0;
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
const
|
|
3052
|
+
const forceIds = Array.isArray(unfinishedSessionIds)
|
|
3053
|
+
? [...new Set(unfinishedSessionIds.filter((id) => typeof id === "string" && id.length > 0))]
|
|
3054
|
+
: [];
|
|
3055
|
+
const forceIncludeSql = forceIds.length > 0
|
|
3056
|
+
? ` OR id IN (${forceIds.map(sqliteStringLiteral).join(",")})`
|
|
3057
|
+
: "";
|
|
3058
|
+
// Fetch sessions that started at/after the cursor, sessions that are still
|
|
3059
|
+
// in-progress (ended_at IS NULL), OR sessions that were previously observed
|
|
3060
|
+
// unfinished. Hermes updates token counts in real-time, including a final
|
|
3061
|
+
// delta when an active session later gets ended_at set.
|
|
3062
|
+
const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
|
|
3020
3063
|
let raw;
|
|
3021
3064
|
try {
|
|
3022
3065
|
raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
|
|
@@ -3037,122 +3080,173 @@ function readHermesSessions(dbPath, lastCompletedEpoch) {
|
|
|
3037
3080
|
return Array.isArray(rows) ? rows : [];
|
|
3038
3081
|
}
|
|
3039
3082
|
|
|
3040
|
-
|
|
3083
|
+
function hasLegacyHermesDefaultState(hermesState) {
|
|
3084
|
+
return (
|
|
3085
|
+
typeof hermesState.lastStartedAt === "number" ||
|
|
3086
|
+
typeof hermesState.lastCompletedStartedAt === "number" ||
|
|
3087
|
+
(hermesState.snapshots && typeof hermesState.snapshots === "object")
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath, onProgress }) {
|
|
3041
3092
|
await ensureDir(path.dirname(queuePath));
|
|
3042
3093
|
const hermesState = cursors.hermes && typeof cursors.hermes === "object" ? cursors.hermes : {};
|
|
3043
3094
|
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
const lastCompletedStartedAt =
|
|
3048
|
-
typeof hermesState.lastCompletedStartedAt === "number" ? hermesState.lastCompletedStartedAt : 0;
|
|
3049
|
-
|
|
3050
|
-
// Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
|
|
3051
|
-
const prevSnapshots = (hermesState.snapshots && typeof hermesState.snapshots === "object")
|
|
3052
|
-
? hermesState.snapshots : {};
|
|
3053
|
-
|
|
3054
|
-
const resolvedDbPath = dbPath || resolveHermesDbPath();
|
|
3055
|
-
if (!fssync.existsSync(resolvedDbPath)) {
|
|
3056
|
-
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
const rows = readHermesSessions(resolvedDbPath, lastCompletedStartedAt);
|
|
3060
|
-
if (rows.length === 0) {
|
|
3061
|
-
cursors.hermes = { ...hermesState, lastCompletedStartedAt, updatedAt: new Date().toISOString() };
|
|
3095
|
+
const dbPaths = resolveAllHermesDBPaths({ hermesPath, dbPath });
|
|
3096
|
+
if (dbPaths.default === null && Object.keys(dbPaths.profiles).length === 0) {
|
|
3097
|
+
// No state in any profile
|
|
3062
3098
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
3063
3099
|
}
|
|
3064
3100
|
|
|
3065
3101
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
3066
|
-
const touchedBuckets = new Set();
|
|
3067
3102
|
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
3103
|
+
const updatedAt = new Date().toISOString();
|
|
3104
|
+
let recordsProcessed = 0;
|
|
3068
3105
|
let eventsAggregated = 0;
|
|
3069
|
-
|
|
3070
|
-
const nextSnapshots = {};
|
|
3106
|
+
const touchedBuckets = new Set();
|
|
3071
3107
|
|
|
3072
|
-
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3108
|
+
function ingestProfile(dbPath, dbState) {
|
|
3109
|
+
const trackedUnfinishedSessionIds = Array.isArray(dbState.unfinishedSessionIds)
|
|
3110
|
+
? dbState.unfinishedSessionIds
|
|
3111
|
+
: [];
|
|
3112
|
+
const rows = readHermesSessions(dbPath, dbState.lastCompletedStartedAt, trackedUnfinishedSessionIds);
|
|
3113
|
+
recordsProcessed += rows.length;
|
|
3114
|
+
if (rows.length === 0) {
|
|
3115
|
+
dbState.updatedAt = updatedAt;
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
|
|
3120
|
+
const prevSnapshots = (dbState.snapshots && typeof dbState.snapshots === "object")
|
|
3121
|
+
? dbState.snapshots : {};
|
|
3122
|
+
|
|
3123
|
+
// Only advance past sessions that have fully ended. Active sessions
|
|
3124
|
+
// (ended_at IS NULL) must be re-read every sync because Hermes updates
|
|
3125
|
+
// their token counts in real-time after each turn.
|
|
3126
|
+
const lastCompletedStartedAt =
|
|
3127
|
+
typeof dbState.lastCompletedStartedAt === "number" ? dbState.lastCompletedStartedAt : 0;
|
|
3128
|
+
|
|
3129
|
+
let maxCompletedStartedAt = lastCompletedStartedAt;
|
|
3130
|
+
let oldestUnfinishedStartedAt = Infinity;
|
|
3131
|
+
const nextUnfinishedSessionIds = new Set();
|
|
3132
|
+
const nextSnapshots = {};
|
|
3133
|
+
|
|
3134
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3135
|
+
const row = rows[i];
|
|
3136
|
+
const inputTokens = toNonNegativeInt(row.input_tokens);
|
|
3137
|
+
const outputTokens = toNonNegativeInt(row.output_tokens);
|
|
3138
|
+
const cacheRead = toNonNegativeInt(row.cache_read_tokens);
|
|
3139
|
+
const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
|
|
3140
|
+
const reasoning = toNonNegativeInt(row.reasoning_tokens);
|
|
3141
|
+
const messageCount = toNonNegativeInt(row.message_count);
|
|
3142
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
|
|
3143
|
+
|
|
3144
|
+
// Save current snapshot for next sync
|
|
3145
|
+
nextSnapshots[row.id] = { in: inputTokens, out: outputTokens, cacheRead, cacheWrite, reasoning, message_count: messageCount };
|
|
3146
|
+
|
|
3147
|
+
const startedAt = Number(row.started_at);
|
|
3148
|
+
const endedAt = row.ended_at == null ? null : Number(row.ended_at);
|
|
3149
|
+
if (endedAt == null) {
|
|
3150
|
+
if (row.id && Number.isFinite(startedAt)) {
|
|
3151
|
+
nextUnfinishedSessionIds.add(row.id);
|
|
3152
|
+
oldestUnfinishedStartedAt = Math.min(oldestUnfinishedStartedAt, startedAt);
|
|
3153
|
+
}
|
|
3154
|
+
} else if (Number.isFinite(startedAt) && startedAt > maxCompletedStartedAt) {
|
|
3155
|
+
maxCompletedStartedAt = startedAt;
|
|
3156
|
+
}
|
|
3109
3157
|
|
|
3110
|
-
|
|
3158
|
+
// Compute delta from previous snapshot (if any) so that we only count
|
|
3159
|
+
// new usage since the last sync. First time we see a session the
|
|
3160
|
+
// previous snapshot is absent, so the full amount is the delta.
|
|
3161
|
+
const prev = prevSnapshots[row.id];
|
|
3162
|
+
let dInput = inputTokens;
|
|
3163
|
+
let dOutput = outputTokens;
|
|
3164
|
+
let dCacheRead = cacheRead;
|
|
3165
|
+
let dCacheWrite = cacheWrite;
|
|
3166
|
+
let dReasoning = reasoning;
|
|
3167
|
+
let dMessageCount = messageCount;
|
|
3168
|
+
if (prev) {
|
|
3169
|
+
dInput = Math.max(0, inputTokens - (prev.in || 0));
|
|
3170
|
+
dOutput = Math.max(0, outputTokens - (prev.out || 0));
|
|
3171
|
+
dCacheRead = Math.max(0, cacheRead - (prev.cacheRead || 0));
|
|
3172
|
+
dCacheWrite = Math.max(0, cacheWrite - (prev.cacheWrite || 0));
|
|
3173
|
+
dReasoning = Math.max(0, reasoning - (prev.reasoning || 0));
|
|
3174
|
+
dMessageCount = Math.max(0, messageCount - (prev.message_count || 0));
|
|
3175
|
+
}
|
|
3176
|
+
// Skip if delta is zero (session unchanged since last sync)
|
|
3177
|
+
if (dInput === 0 && dOutput === 0 && dCacheRead === 0 && dCacheWrite === 0 && dReasoning === 0) continue;
|
|
3111
3178
|
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
total_tokens: dInput + dOutput + dCacheRead + dCacheWrite + dReasoning,
|
|
3119
|
-
conversation_count: toNonNegativeInt(row.message_count) || 1,
|
|
3120
|
-
};
|
|
3179
|
+
// Prefer ended_at for bucket placement; fall back to started_at
|
|
3180
|
+
const epochSec = endedAt ?? startedAt;
|
|
3181
|
+
if (!epochSec || !Number.isFinite(epochSec)) continue;
|
|
3182
|
+
const tsIso = new Date(epochSec * 1000).toISOString();
|
|
3183
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
3184
|
+
if (!bucketStart) continue;
|
|
3121
3185
|
|
|
3122
|
-
|
|
3123
|
-
addTotals(bucket.totals, delta);
|
|
3124
|
-
touchedBuckets.add(bucketKey("hermes", model, bucketStart));
|
|
3125
|
-
eventsAggregated++;
|
|
3186
|
+
const model = normalizeModelInput(row.model) || "hermes-agent";
|
|
3126
3187
|
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3188
|
+
const delta = {
|
|
3189
|
+
input_tokens: dInput,
|
|
3190
|
+
cached_input_tokens: dCacheRead,
|
|
3191
|
+
cache_creation_input_tokens: dCacheWrite,
|
|
3192
|
+
output_tokens: dOutput,
|
|
3193
|
+
reasoning_output_tokens: dReasoning,
|
|
3194
|
+
total_tokens: dInput + dOutput + dCacheRead + dCacheWrite + dReasoning,
|
|
3195
|
+
conversation_count: dMessageCount,
|
|
3196
|
+
};
|
|
3131
3197
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3198
|
+
const bucket = getHourlyBucket(hourlyState, "hermes", model, bucketStart);
|
|
3199
|
+
addTotals(bucket.totals, delta);
|
|
3200
|
+
touchedBuckets.add(bucketKey("hermes", model, bucketStart));
|
|
3201
|
+
eventsAggregated++;
|
|
3202
|
+
|
|
3203
|
+
if (cb) {
|
|
3204
|
+
cb({
|
|
3205
|
+
index: i + 1,
|
|
3206
|
+
total: rows.length,
|
|
3207
|
+
recordsProcessed: i + 1,
|
|
3208
|
+
eventsAggregated,
|
|
3209
|
+
bucketsQueued: touchedBuckets.size,
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3140
3212
|
}
|
|
3213
|
+
|
|
3214
|
+
const nextLastCompletedStartedAt = Number.isFinite(oldestUnfinishedStartedAt)
|
|
3215
|
+
? Math.min(maxCompletedStartedAt, oldestUnfinishedStartedAt)
|
|
3216
|
+
: maxCompletedStartedAt;
|
|
3217
|
+
|
|
3218
|
+
Object.assign(dbState, {
|
|
3219
|
+
lastStartedAt: nextLastCompletedStartedAt,
|
|
3220
|
+
lastCompletedStartedAt: nextLastCompletedStartedAt,
|
|
3221
|
+
unfinishedSessionIds: Array.from(nextUnfinishedSessionIds),
|
|
3222
|
+
snapshots: nextSnapshots,
|
|
3223
|
+
updatedAt,
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
if (dbPaths.default) {
|
|
3228
|
+
ingestProfile(dbPaths.default, hermesState);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
hermesState.profiles = hermesState.profiles && typeof hermesState.profiles === "object" ? hermesState.profiles : {};
|
|
3232
|
+
|
|
3233
|
+
for (const [profileName, dbPath] of Object.entries(dbPaths.profiles)) {
|
|
3234
|
+
const profileState = hermesState.profiles[profileName] && typeof hermesState.profiles[profileName] === "object"
|
|
3235
|
+
? hermesState.profiles[profileName]
|
|
3236
|
+
: {};
|
|
3237
|
+
hermesState.profiles[profileName] = profileState;
|
|
3238
|
+
ingestProfile(dbPath, profileState);
|
|
3141
3239
|
}
|
|
3142
3240
|
|
|
3143
3241
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
3144
|
-
const updatedAt = new Date().toISOString();
|
|
3145
3242
|
hourlyState.updatedAt = updatedAt;
|
|
3146
3243
|
cursors.hourly = hourlyState;
|
|
3147
3244
|
cursors.hermes = {
|
|
3148
3245
|
...hermesState,
|
|
3149
|
-
|
|
3150
|
-
lastCompletedStartedAt: maxCompletedStartedAt,
|
|
3151
|
-
snapshots: nextSnapshots,
|
|
3152
|
-
updatedAt,
|
|
3246
|
+
updatedAt, // Update the overall profile state timestamp even if the DB doesn't exist for the fast-path check
|
|
3153
3247
|
};
|
|
3154
3248
|
|
|
3155
|
-
return { recordsProcessed
|
|
3249
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
3156
3250
|
}
|
|
3157
3251
|
|
|
3158
3252
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -5893,15 +5987,15 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
|
|
|
5893
5987
|
}
|
|
5894
5988
|
|
|
5895
5989
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5896
|
-
// Grok Build (xAI) — passive reader for ~/.grok/sessions/**/
|
|
5990
|
+
// Grok Build (xAI) — passive reader for ~/.grok/sessions/**/updates.jsonl + signals.json
|
|
5897
5991
|
// Triggered either by full scan in sync or by the SessionEnd hook writing a signal.
|
|
5898
|
-
//
|
|
5899
|
-
//
|
|
5900
|
-
//
|
|
5992
|
+
// updates.jsonl exposes cumulative totalTokens metadata. Grok still does not
|
|
5993
|
+
// expose a stable prompt/output/cache split locally, so these rows keep the
|
|
5994
|
+
// estimated input/output split while using better local telemetry for totals.
|
|
5901
5995
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5902
5996
|
|
|
5903
5997
|
const GROK_ESTIMATED_INPUT_RATIO = 0.8;
|
|
5904
|
-
const GROK_CURSOR_VERSION =
|
|
5998
|
+
const GROK_CURSOR_VERSION = 3;
|
|
5905
5999
|
|
|
5906
6000
|
function resolveGrokBuildHome(env = process.env) {
|
|
5907
6001
|
return (
|
|
@@ -5936,9 +6030,11 @@ function resolveGrokBuildSessions(env = process.env) {
|
|
|
5936
6030
|
for (const sid of sessionIds) {
|
|
5937
6031
|
const sessionDir = path.join(cwdPath, sid);
|
|
5938
6032
|
const signalsPath = path.join(sessionDir, "signals.json");
|
|
5939
|
-
|
|
6033
|
+
const updatesPath = path.join(sessionDir, "updates.jsonl");
|
|
6034
|
+
if (fssync.existsSync(signalsPath) || fssync.existsSync(updatesPath)) {
|
|
5940
6035
|
results.push({
|
|
5941
6036
|
sessionDir,
|
|
6037
|
+
updatesPath,
|
|
5942
6038
|
signalsPath,
|
|
5943
6039
|
summaryPath: path.join(sessionDir, "summary.json"),
|
|
5944
6040
|
sessionId: sid,
|
|
@@ -5961,7 +6057,11 @@ function normalizeGrokSessionSnapshots(grokState) {
|
|
|
5961
6057
|
totalTokens,
|
|
5962
6058
|
messageCount: normalizeNonNegativeNumber(snapshot.messageCount),
|
|
5963
6059
|
model: normalizeModelInput(snapshot.model) || null,
|
|
6060
|
+
source: normalizeModelInput(snapshot.source) || null,
|
|
6061
|
+
lastEventId: normalizeModelInput(snapshot.lastEventId) || null,
|
|
6062
|
+
lastEventTimestamp: normalizeModelInput(snapshot.lastEventTimestamp) || null,
|
|
5964
6063
|
updatedAt: normalizeModelInput(snapshot.updatedAt) || null,
|
|
6064
|
+
legacySeen: snapshot.legacySeen === true,
|
|
5965
6065
|
};
|
|
5966
6066
|
}
|
|
5967
6067
|
}
|
|
@@ -5971,7 +6071,7 @@ function normalizeGrokSessionSnapshots(grokState) {
|
|
|
5971
6071
|
const safeSessionId = normalizeModelInput(sessionId);
|
|
5972
6072
|
if (!safeSessionId || snapshots[safeSessionId]) continue;
|
|
5973
6073
|
snapshots[safeSessionId] = {
|
|
5974
|
-
totalTokens:
|
|
6074
|
+
totalTokens: 0,
|
|
5975
6075
|
messageCount: 0,
|
|
5976
6076
|
model: null,
|
|
5977
6077
|
updatedAt: normalizeModelInput(grokState.updatedAt) || null,
|
|
@@ -5998,6 +6098,14 @@ function readGrokJsonFile(filePath) {
|
|
|
5998
6098
|
}
|
|
5999
6099
|
}
|
|
6000
6100
|
|
|
6101
|
+
function grokUpdatesPathForSession(sess) {
|
|
6102
|
+
if (typeof sess?.updatesPath === "string" && sess.updatesPath.trim()) return sess.updatesPath;
|
|
6103
|
+
if (typeof sess?.sessionDir === "string" && sess.sessionDir.trim()) {
|
|
6104
|
+
return path.join(sess.sessionDir, "updates.jsonl");
|
|
6105
|
+
}
|
|
6106
|
+
return null;
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6001
6109
|
function grokSessionIdFor(sess) {
|
|
6002
6110
|
return (
|
|
6003
6111
|
normalizeModelInput(sess?.sessionId) ||
|
|
@@ -6025,11 +6133,113 @@ function grokLastActiveFromSignals(signals, summary) {
|
|
|
6025
6133
|
);
|
|
6026
6134
|
}
|
|
6027
6135
|
|
|
6028
|
-
function
|
|
6136
|
+
function grokMessageCountFromSignals(signals) {
|
|
6137
|
+
return normalizeNonNegativeNumber(
|
|
6138
|
+
signals?.assistantMessageCount ??
|
|
6139
|
+
signals?.turnCount ??
|
|
6140
|
+
signals?.num_chat_messages ??
|
|
6141
|
+
signals?.messageCount,
|
|
6142
|
+
);
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
function grokEffectiveTotalFromSignals(signals) {
|
|
6146
|
+
if (!signals || typeof signals !== "object") return 0;
|
|
6147
|
+
const beforeCompaction = normalizeNonNegativeNumber(signals.totalTokensBeforeCompaction);
|
|
6148
|
+
const totalTokens = normalizeNonNegativeNumber(signals.totalTokens);
|
|
6149
|
+
if (signals.contextTokensUsed == null) {
|
|
6150
|
+
return beforeCompaction + totalTokens;
|
|
6151
|
+
}
|
|
6152
|
+
return Math.max(
|
|
6153
|
+
totalTokens,
|
|
6154
|
+
beforeCompaction + normalizeNonNegativeNumber(signals.contextTokensUsed),
|
|
6155
|
+
);
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
function grokTimestampToIso(value) {
|
|
6159
|
+
if (value == null) return null;
|
|
6160
|
+
if (typeof value === "number") {
|
|
6161
|
+
if (!Number.isFinite(value) || value <= 0) return null;
|
|
6162
|
+
const millis = value < 10_000_000_000 ? value * 1000 : value;
|
|
6163
|
+
const dt = new Date(millis);
|
|
6164
|
+
return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
|
|
6165
|
+
}
|
|
6166
|
+
if (typeof value === "string") {
|
|
6167
|
+
const trimmed = value.trim();
|
|
6168
|
+
if (!trimmed) return null;
|
|
6169
|
+
if (/^[0-9]+(?:\.[0-9]+)?$/.test(trimmed)) {
|
|
6170
|
+
return grokTimestampToIso(Number(trimmed));
|
|
6171
|
+
}
|
|
6172
|
+
const dt = new Date(trimmed);
|
|
6173
|
+
return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
|
|
6174
|
+
}
|
|
6175
|
+
return null;
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
function grokTimestampFromUpdate(meta, record, fallback) {
|
|
6179
|
+
return (
|
|
6180
|
+
grokTimestampToIso(meta?.agentTimestampMs) ||
|
|
6181
|
+
grokTimestampToIso(meta?.timestampMs) ||
|
|
6182
|
+
grokTimestampToIso(record?.timestamp_ms) ||
|
|
6183
|
+
grokTimestampToIso(record?.timestamp) ||
|
|
6184
|
+
grokTimestampToIso(record?.time) ||
|
|
6185
|
+
fallback ||
|
|
6186
|
+
null
|
|
6187
|
+
);
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
function grokEventId(value, fallback) {
|
|
6191
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
6192
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
6193
|
+
return fallback;
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp) {
|
|
6197
|
+
if (!updatesPath) return { events: [], recordsProcessed: 0 };
|
|
6198
|
+
try {
|
|
6199
|
+
const stat = fssync.statSync(updatesPath);
|
|
6200
|
+
if (!stat.isFile()) return { events: [], recordsProcessed: 0 };
|
|
6201
|
+
} catch {
|
|
6202
|
+
return { events: [], recordsProcessed: 0 };
|
|
6203
|
+
}
|
|
6204
|
+
|
|
6205
|
+
const events = [];
|
|
6206
|
+
let lineIndex = 0;
|
|
6207
|
+
const input = fssync.createReadStream(updatesPath, { encoding: "utf8" });
|
|
6208
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
6209
|
+
try {
|
|
6210
|
+
for await (const line of rl) {
|
|
6211
|
+
lineIndex++;
|
|
6212
|
+
if (!line || !line.trim()) continue;
|
|
6213
|
+
let record;
|
|
6214
|
+
try {
|
|
6215
|
+
record = JSON.parse(line);
|
|
6216
|
+
} catch {
|
|
6217
|
+
continue;
|
|
6218
|
+
}
|
|
6219
|
+
const meta = record?.params?._meta || record?._meta;
|
|
6220
|
+
if (!meta || typeof meta !== "object") continue;
|
|
6221
|
+
const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
|
|
6222
|
+
if (totalTokens <= 0) continue;
|
|
6223
|
+
const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
|
|
6224
|
+
events.push({
|
|
6225
|
+
totalTokens,
|
|
6226
|
+
timestamp,
|
|
6227
|
+
eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
|
|
6228
|
+
});
|
|
6229
|
+
}
|
|
6230
|
+
} catch {
|
|
6231
|
+
return { events, recordsProcessed: events.length };
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
return { events, recordsProcessed: events.length };
|
|
6235
|
+
}
|
|
6236
|
+
|
|
6237
|
+
function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
6029
6238
|
const total = Math.trunc(normalizeNonNegativeNumber(totalTokens));
|
|
6030
6239
|
const inputTokens = Math.round(total * GROK_ESTIMATED_INPUT_RATIO);
|
|
6031
6240
|
const outputTokens = Math.max(0, total - inputTokens);
|
|
6032
|
-
const
|
|
6241
|
+
const rawConversations = Math.trunc(normalizeNonNegativeNumber(conversationCount));
|
|
6242
|
+
const conversations = options.allowZeroConversationCount ? rawConversations : Math.max(1, rawConversations);
|
|
6033
6243
|
|
|
6034
6244
|
return {
|
|
6035
6245
|
input_tokens: inputTokens,
|
|
@@ -6062,51 +6272,103 @@ async function parseGrokBuildIncremental({
|
|
|
6062
6272
|
|
|
6063
6273
|
let eventsAggregated = 0;
|
|
6064
6274
|
|
|
6065
|
-
for (
|
|
6275
|
+
for (let index = 0; index < sessionList.length; index++) {
|
|
6276
|
+
const sess = sessionList[index];
|
|
6066
6277
|
const sessionId = grokSessionIdFor(sess);
|
|
6067
|
-
if (!sessionId)
|
|
6278
|
+
if (!sessionId) {
|
|
6279
|
+
if (onProgress) onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
|
|
6280
|
+
continue;
|
|
6281
|
+
}
|
|
6068
6282
|
|
|
6069
6283
|
const signals = sess?.signals && typeof sess.signals === "object"
|
|
6070
6284
|
? sess.signals
|
|
6071
6285
|
: readGrokJsonFile(sess?.signalsPath);
|
|
6072
|
-
|
|
6286
|
+
const safeSignals = signals && typeof signals === "object" ? signals : {};
|
|
6073
6287
|
|
|
6074
6288
|
const summary = sess?.summary && typeof sess.summary === "object"
|
|
6075
6289
|
? sess.summary
|
|
6076
6290
|
: readGrokJsonFile(sess?.summaryPath) || {};
|
|
6077
|
-
const totalTokens = normalizeNonNegativeNumber(signals.contextTokensUsed ?? signals.totalTokens);
|
|
6078
|
-
if (totalTokens <= 0) {
|
|
6079
|
-
continue;
|
|
6080
|
-
}
|
|
6081
|
-
|
|
6082
6291
|
const previous = sessionSnapshots[sessionId] || {};
|
|
6083
6292
|
const previousTotal = normalizeNonNegativeNumber(previous.totalTokens);
|
|
6084
|
-
const deltaTokens = totalTokens > previousTotal ? totalTokens - previousTotal : 0;
|
|
6085
|
-
if (deltaTokens <= 0) continue;
|
|
6086
|
-
|
|
6087
|
-
const messageCount = normalizeNonNegativeNumber(
|
|
6088
|
-
signals.assistantMessageCount ?? signals.num_chat_messages ?? signals.messageCount,
|
|
6089
|
-
);
|
|
6090
6293
|
const previousMessageCount = normalizeNonNegativeNumber(previous.messageCount);
|
|
6091
|
-
const
|
|
6092
|
-
|
|
6093
|
-
const
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6294
|
+
const messageCount = grokMessageCountFromSignals(safeSignals);
|
|
6295
|
+
const model = grokModelFromSignals(safeSignals);
|
|
6296
|
+
const lastActive = grokLastActiveFromSignals(safeSignals, summary);
|
|
6297
|
+
|
|
6298
|
+
let highWatermark = previousTotal;
|
|
6299
|
+
let observedTotal = previousTotal;
|
|
6300
|
+
let tokenDeltaForSession = 0;
|
|
6301
|
+
let finalTouchedHourStart = null;
|
|
6302
|
+
let source = previous.source || null;
|
|
6303
|
+
let lastEventId = previous.lastEventId || null;
|
|
6304
|
+
let lastEventTimestamp = previous.lastEventTimestamp || null;
|
|
6305
|
+
const pendingTokenDeltas = [];
|
|
6306
|
+
|
|
6307
|
+
const recordTokenDelta = (deltaTokens, timestamp, deltaSource) => {
|
|
6308
|
+
const hourStartStr = toUtcHalfHourStart(timestamp) || toUtcHalfHourStart(Date.now());
|
|
6309
|
+
if (!hourStartStr) return false;
|
|
6310
|
+
pendingTokenDeltas.push({ deltaTokens, hourStartStr });
|
|
6311
|
+
tokenDeltaForSession += deltaTokens;
|
|
6312
|
+
finalTouchedHourStart = hourStartStr;
|
|
6313
|
+
source = deltaSource;
|
|
6314
|
+
lastEventTimestamp = timestamp || lastEventTimestamp;
|
|
6315
|
+
return true;
|
|
6109
6316
|
};
|
|
6317
|
+
|
|
6318
|
+
const updates = await readGrokUpdateTokenEvents(grokUpdatesPathForSession(sess), lastActive);
|
|
6319
|
+
for (const event of updates.events) {
|
|
6320
|
+
observedTotal = Math.max(observedTotal, event.totalTokens);
|
|
6321
|
+
lastEventId = event.eventId || lastEventId;
|
|
6322
|
+
lastEventTimestamp = event.timestamp || lastEventTimestamp;
|
|
6323
|
+
if (event.totalTokens <= highWatermark) continue;
|
|
6324
|
+
const deltaTokens = event.totalTokens - highWatermark;
|
|
6325
|
+
highWatermark = event.totalTokens;
|
|
6326
|
+
recordTokenDelta(deltaTokens, event.timestamp || lastActive, "updates");
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
|
|
6330
|
+
observedTotal = Math.max(observedTotal, effectiveSignalTotal);
|
|
6331
|
+
if (effectiveSignalTotal > highWatermark) {
|
|
6332
|
+
const deltaTokens = effectiveSignalTotal - highWatermark;
|
|
6333
|
+
highWatermark = effectiveSignalTotal;
|
|
6334
|
+
recordTokenDelta(deltaTokens, lastActive, "signals");
|
|
6335
|
+
}
|
|
6336
|
+
|
|
6337
|
+
const finalTotal = Math.max(previousTotal, highWatermark, observedTotal);
|
|
6338
|
+
const legacyBaselineOnly = previous.legacySeen && previousTotal === 0 && finalTotal > 0;
|
|
6339
|
+
if (!legacyBaselineOnly) {
|
|
6340
|
+
for (const pending of pendingTokenDeltas) {
|
|
6341
|
+
const delta = estimateGrokTokenDelta(pending.deltaTokens, 0, { allowZeroConversationCount: true });
|
|
6342
|
+
const bucket = getHourlyBucket(hourlyState, "grok", model, pending.hourStartStr);
|
|
6343
|
+
addTotals(bucket.totals, delta);
|
|
6344
|
+
touchedBuckets.add(bucketKey("grok", model, pending.hourStartStr));
|
|
6345
|
+
eventsAggregated++;
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
if (!legacyBaselineOnly && tokenDeltaForSession > 0 && finalTouchedHourStart) {
|
|
6350
|
+
const deltaMessageCount =
|
|
6351
|
+
messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
|
|
6352
|
+
const bucket = getHourlyBucket(hourlyState, "grok", model, finalTouchedHourStart);
|
|
6353
|
+
addTotals(bucket.totals, { conversation_count: deltaMessageCount });
|
|
6354
|
+
touchedBuckets.add(bucketKey("grok", model, finalTouchedHourStart));
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
if (finalTotal > 0 && (tokenDeltaForSession > 0 || previousTotal > 0 || legacyBaselineOnly)) {
|
|
6358
|
+
sessionSnapshots[sessionId] = {
|
|
6359
|
+
totalTokens: finalTotal,
|
|
6360
|
+
messageCount: Math.max(previousMessageCount, messageCount),
|
|
6361
|
+
model,
|
|
6362
|
+
source: source || previous.source || null,
|
|
6363
|
+
lastEventId,
|
|
6364
|
+
lastEventTimestamp,
|
|
6365
|
+
updatedAt: new Date().toISOString(),
|
|
6366
|
+
};
|
|
6367
|
+
}
|
|
6368
|
+
|
|
6369
|
+
if (onProgress) {
|
|
6370
|
+
onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
|
|
6371
|
+
}
|
|
6110
6372
|
}
|
|
6111
6373
|
|
|
6112
6374
|
const bucketsQueued = queuePath
|
|
@@ -6229,6 +6491,7 @@ async function parseAntigravityIncremental({
|
|
|
6229
6491
|
const sameFile = prev && prev.inode === inode;
|
|
6230
6492
|
const lastLine = sameFile ? Number(prev.lastLine || 0) : 0;
|
|
6231
6493
|
const initialContextTokens = sameFile ? Number(prev.contextTokens || 0) : 0;
|
|
6494
|
+
const initialPrevContext = sameFile ? Number(prev.previousContextTokens || 0) : 0;
|
|
6232
6495
|
const initialModel = sameFile && typeof prev.currentModel === "string" ? prev.currentModel : null;
|
|
6233
6496
|
|
|
6234
6497
|
const projectContext = projectEnabled
|
|
@@ -6247,6 +6510,7 @@ async function parseAntigravityIncremental({
|
|
|
6247
6510
|
filePath,
|
|
6248
6511
|
lastLine,
|
|
6249
6512
|
initialContextTokens,
|
|
6513
|
+
initialPrevContext,
|
|
6250
6514
|
initialModel,
|
|
6251
6515
|
hourlyState,
|
|
6252
6516
|
touchedBuckets,
|
|
@@ -6263,6 +6527,7 @@ async function parseAntigravityIncremental({
|
|
|
6263
6527
|
mtimeMs,
|
|
6264
6528
|
lastLine: result.lastLine,
|
|
6265
6529
|
contextTokens: result.contextTokens,
|
|
6530
|
+
previousContextTokens: result.previousContextTokens,
|
|
6266
6531
|
currentModel: result.currentModel,
|
|
6267
6532
|
updatedAt: new Date().toISOString(),
|
|
6268
6533
|
};
|
|
@@ -6300,6 +6565,7 @@ async function parseAntigravityFile({
|
|
|
6300
6565
|
filePath,
|
|
6301
6566
|
lastLine,
|
|
6302
6567
|
initialContextTokens,
|
|
6568
|
+
initialPrevContext,
|
|
6303
6569
|
initialModel,
|
|
6304
6570
|
hourlyState,
|
|
6305
6571
|
touchedBuckets,
|
|
@@ -6311,7 +6577,13 @@ async function parseAntigravityFile({
|
|
|
6311
6577
|
}) {
|
|
6312
6578
|
const raw = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
6313
6579
|
if (!raw.trim()) {
|
|
6314
|
-
return {
|
|
6580
|
+
return {
|
|
6581
|
+
lastLine: 0,
|
|
6582
|
+
eventsAggregated: 0,
|
|
6583
|
+
contextTokens: 0,
|
|
6584
|
+
previousContextTokens: 0,
|
|
6585
|
+
currentModel: null,
|
|
6586
|
+
};
|
|
6315
6587
|
}
|
|
6316
6588
|
|
|
6317
6589
|
const lines = raw
|
|
@@ -6325,11 +6597,16 @@ async function parseAntigravityFile({
|
|
|
6325
6597
|
const canResume =
|
|
6326
6598
|
Number.isFinite(lastLine) && lastLine > 0 && lastLine <= lines.length;
|
|
6327
6599
|
const cachedTokens = Number.isFinite(initialContextTokens) ? initialContextTokens : 0;
|
|
6600
|
+
const cachedPrev = Number.isFinite(initialPrevContext) ? initialPrevContext : 0;
|
|
6328
6601
|
const cachedModel = typeof initialModel === "string" ? initialModel : null;
|
|
6329
6602
|
const resumed = canResume && (cachedTokens > 0 || cachedModel !== null);
|
|
6330
6603
|
const scanStart = resumed ? lastLine : 0;
|
|
6331
6604
|
let currentModel = resumed ? cachedModel : null;
|
|
6332
6605
|
let contextTokens = resumed ? cachedTokens : 0;
|
|
6606
|
+
// Snapshot of contextTokens at the last PLANNER_RESPONSE we billed for. Only
|
|
6607
|
+
// tokens accumulated AFTER that point count as new input on the next planner
|
|
6608
|
+
// call — prevents O(N²) double-counting of the full history every turn.
|
|
6609
|
+
let previousContextTokens = resumed ? cachedPrev : 0;
|
|
6333
6610
|
let lastCompletedLine = Math.min(Number.isFinite(lastLine) ? lastLine : 0, lines.length);
|
|
6334
6611
|
|
|
6335
6612
|
for (let i = scanStart; i < lines.length; i++) {
|
|
@@ -6375,22 +6652,28 @@ async function parseAntigravityFile({
|
|
|
6375
6652
|
|
|
6376
6653
|
let model = currentModel || "antigravity-unknown";
|
|
6377
6654
|
let delta = initTotals();
|
|
6655
|
+
let billedPlanner = false;
|
|
6378
6656
|
|
|
6379
6657
|
if (parsed.type === "PLANNER_RESPONSE") {
|
|
6380
6658
|
const content = typeof parsed.content === "string" ? parsed.content : "";
|
|
6381
6659
|
const thinking = typeof parsed.thinking === "string" ? parsed.thinking : "";
|
|
6382
6660
|
|
|
6383
|
-
|
|
6384
|
-
|
|
6661
|
+
const inputDelta = Math.max(0, contextTokens - previousContextTokens);
|
|
6662
|
+
const outputTokens =
|
|
6385
6663
|
antigravityValueTokens(content) + antigravityValueTokens(parsed.tool_calls);
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
delta.
|
|
6664
|
+
const reasoningTokens = antigravityValueTokens(thinking);
|
|
6665
|
+
|
|
6666
|
+
delta.input_tokens = inputDelta;
|
|
6667
|
+
delta.output_tokens = outputTokens;
|
|
6668
|
+
delta.reasoning_output_tokens = reasoningTokens;
|
|
6669
|
+
// Match the mainstream convention (Codebuddy / Kilocode / OMP / Hermes):
|
|
6670
|
+
// total_tokens = sum of every token column. No cache columns here.
|
|
6671
|
+
delta.total_tokens = inputDelta + outputTokens + reasoningTokens;
|
|
6390
6672
|
delta.conversation_count = 1;
|
|
6673
|
+
billedPlanner = delta.total_tokens > 0;
|
|
6391
6674
|
}
|
|
6392
6675
|
|
|
6393
|
-
if (
|
|
6676
|
+
if (!billedPlanner) {
|
|
6394
6677
|
contextTokens += eventContextTokens;
|
|
6395
6678
|
lastCompletedLine = i + 1;
|
|
6396
6679
|
continue;
|
|
@@ -6412,6 +6695,11 @@ async function parseAntigravityFile({
|
|
|
6412
6695
|
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
6413
6696
|
}
|
|
6414
6697
|
eventsAggregated += 1;
|
|
6698
|
+
// Snapshot the pre-planner context first. The planner's own content+tool_calls
|
|
6699
|
+
// (eventContextTokens, added below) become part of the next turn's history,
|
|
6700
|
+
// so they MUST be billed as input on the next planner — don't fold them into
|
|
6701
|
+
// previousContextTokens or that history vanishes from the totals.
|
|
6702
|
+
previousContextTokens = contextTokens;
|
|
6415
6703
|
contextTokens += eventContextTokens;
|
|
6416
6704
|
lastCompletedLine = i + 1;
|
|
6417
6705
|
}
|
|
@@ -6420,6 +6708,7 @@ async function parseAntigravityFile({
|
|
|
6420
6708
|
lastLine: lastCompletedLine,
|
|
6421
6709
|
eventsAggregated,
|
|
6422
6710
|
contextTokens,
|
|
6711
|
+
previousContextTokens,
|
|
6423
6712
|
currentModel,
|
|
6424
6713
|
};
|
|
6425
6714
|
}
|
|
@@ -6505,6 +6794,7 @@ module.exports = {
|
|
|
6505
6794
|
readOpencodeDbMessages,
|
|
6506
6795
|
resolveKiroDbPath,
|
|
6507
6796
|
resolveKiroJsonlPath,
|
|
6797
|
+
resolveHermesPath,
|
|
6508
6798
|
resolveHermesDbPath,
|
|
6509
6799
|
resolveCopilotOtelPaths,
|
|
6510
6800
|
parseRolloutIncremental,
|
|
@@ -6562,7 +6852,7 @@ module.exports = {
|
|
|
6562
6852
|
canonicalizeProjectRef,
|
|
6563
6853
|
deriveProjectKeyFromRef,
|
|
6564
6854
|
|
|
6565
|
-
// Grok Build (xAI) — SessionEnd hook + passive signals.json reader
|
|
6855
|
+
// Grok Build (xAI) — SessionEnd hook + passive updates.jsonl/signals.json reader
|
|
6566
6856
|
resolveGrokBuildHome,
|
|
6567
6857
|
resolveGrokBuildSessions,
|
|
6568
6858
|
parseGrokBuildIncremental,
|