Skip to content

feat(stats): collect token/tool/message metrics for the stats screen - #32

Merged
JeanBaptisteRenard merged 1 commit into
mainfrom
fix/stats-metrics-collection
Jun 2, 2026
Merged

feat(stats): collect token/tool/message metrics for the stats screen#32
JeanBaptisteRenard merged 1 commit into
mainfrom
fix/stats-metrics-collection

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Collaborator

Problème

L'écran de statistiques affichait 0 tokens, 0 tool calls, usage par modèle vide, et un compte de sessions gonflé. Le renderer public/stats-view.js était pourtant déjà câblé pour ces données — c'est le backend qui ne fournissait que messageCount :

  • read-session-file.js ne lisait jamais message.usage, message.model, ni les blocs tool_use.
  • Le schéma session_cache n'avait aucune colonne token/tool/model.
  • getDailyActivity() ne sommait que messageCount, groupé par modified de la session entière (volume d'une session multi-jours versé sur son dernier jour).
  • Les handlers IPC codaient en dur modelUsage: {} et omettaient dailyModelTokens/toolCallCount.
  • Total Sessions = COUNT(*) incluait les subagents (~877 lignes sur ~975).

Solution

Nouvelle table session_metrics : une ligne par (sessionId, date, model), bucketée par le timestamp de chaque message (pas par le modified de la session).

  • read-session-file.jsextractDailyMetrics() (helper pur, testable) accumule tokens (input/output/cache), tool_use, et messages. Les tours assistant <synthetic>/sans modèle vont dans le bucket '' sans tokens ; les tours user purement tool_result ne sont pas comptés comme messages (évite le gonflement constaté à 116991 msg/jour). dailyMetrics attaché aux objets parent + subagent.
  • db.js — migration v5 (session_metrics + index date) ; replaceSessionMetrics() (delete-by-session puis insert, transactionnel) ; suppression câblée dans deleteCachedSession/deleteCachedFolder (l'effacement par folder sous-sélectionne session_cache, donc il passe en premier) ; agrégats getDailyMetrics / getDailyModelTokens / getModelUsage / getTotalCounts (totalSessions = sessions parentes uniquement).
  • session-cache.js — écrit les métriques sur les seuls chemins de lecture complète (branche NEW-file du refresh + handler du worker), pas sur le refresh header-only.
  • main.jsget-stats-from-db / refresh-stats construisent l'objet stats complet via buildStatsFromDb().
  • public/stats-view.js — deux cartes ajoutées (Total Tokens, Tool Calls) + mise en forme K/M/B factorisée.

Pas de backfill : les métriques se peuplent au prochain cold-start (le worker scan-projects.js relit tous les JSONL à chaque démarrage). La migration v5 ne purge volontairement pas session_cache.

Tests

  • test/read-session-file-metrics.test.js — unit extractDailyMetrics + intégration readSessionFile.
  • test/db-session-metrics.test.js — miroir pur-JS de l'agrégation SQL (better-sqlite3 ne charge pas sous node:test).
  • task check : 104/104 tests, 0 fail, 0 erreur lint. Extraction vérifiée sur un JSONL réel (140 msg, 58 tool calls, modèle claude-opus-4-8).

🤖 Generated with Claude Code

The stats screen renderer was already wired to display tokens, tool calls,
and per-model usage, but the backend only fed messageCount — so those cards
showed zeros and Total Sessions was inflated by subagents.

- read-session-file.js: extractDailyMetrics() accumulates per-(date,model)
  tokens (input/output/cache), tool_use counts, and message counts bucketed
  by each line's timestamp. Synthetic/model-less assistant turns bucket under
  model '' with no tokens; tool_result-only user turns are not counted as
  messages. dailyMetrics attached to parent + subagent return objects.
- db.js: migration v5 adds session_metrics(sessionId,date,model,...) +
  date index; replaceSessionMetrics() (delete-by-session then insert, in a
  txn); deletion wired into deleteCachedSession/deleteCachedFolder (folder
  delete sub-selects on session_cache, so it runs first); aggregates
  getDailyMetrics/getDailyModelTokens/getModelUsage/getTotalCounts
  (totalSessions = parents only).
- session-cache.js: writes metrics on the full-read paths only (NEW-file
  branch of refreshFolder + the worker handler), not the header-only refresh.
- main.js: get-stats-from-db / refresh-stats now build the full stats object
  (dailyActivity, dailyModelTokens, modelUsage, totals) from session_metrics.
- public/stats-view.js: two new summary cards (Total Tokens, Tool Calls).

Tests: test/read-session-file-metrics.test.js (extractDailyMetrics unit +
readSessionFile integration), test/db-session-metrics.test.js (pure-JS mirror
of the SQL aggregation). Metrics populate automatically on next cold-start
rebuild — no separate backfill.
@JeanBaptisteRenard
JeanBaptisteRenard merged commit c83043e into main Jun 2, 2026
7 checks passed
@JeanBaptisteRenard
JeanBaptisteRenard deleted the fix/stats-metrics-collection branch June 2, 2026 00:25
JeanBaptisteRenard added a commit that referenced this pull request Jun 9, 2026
…-32-shell-injection

security: harden scheduler/auth/mcp against shell injection (port upstream #32)
abasiri pushed a commit that referenced this pull request Aug 1, 2026
…ll strings (#32)

Session and scheduled-task commands were assembled by concatenating user-controlled values into a shell string. Values now go into an argv array quoted at the boundary by quoteArgvForShell(), so a crafted worktree name, permission mode, add-dir, or schedule frontmatter field can no longer inject either a shell command or an extra CLI flag (e.g. --dangerously-skip-permissions).

Also:
- claude-auth.js reads the Keychain via execFileSync, so $USER is never interpolated into a command string.
- Drops the --append-system-prompt "$(cat '/tmp/...')" trick, which was a live command-substitution site and left the system prompt in a world-readable temp file that was never cleaned up.
- preLaunchCmd stays raw shell by design (e.g. 'aws-vault exec profile --'); only newlines are rejected.

Adds test/schedule-injection.test.js covering bash and PowerShell quoting.

Co-authored-by: joeytwiddle <joeytwiddle@gmail.com>
abasiri added a commit that referenced this pull request Aug 1, 2026
Branch was 14 behind. The conflict was in the block this PR wraps: #32 landed
on main and changed runScheduleCommand from a shell string to argv
(runScheduleCommand(claudeArgv, …) plus quoteArgvForShell). This branch still
carried the pre-#32 string form inside its new single-instance wrapper, so a
naive resolution would have silently reverted the shell-injection fix.

Resolved by keeping main's argv-based whenReady body and wrapping it in this
PR's requestSingleInstanceLock else-branch. Verified with `git diff -w` against
main: the only semantic change is the lock; everything else is the one-level
reindent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant