feat(stats): collect token/tool/message metrics for the stats screen - #32
Merged
Merged
Conversation
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
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)
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 quemessageCount:read-session-file.jsne lisait jamaismessage.usage,message.model, ni les blocstool_use.session_cachen'avait aucune colonne token/tool/model.getDailyActivity()ne sommait quemessageCount, groupé parmodifiedde la session entière (volume d'une session multi-jours versé sur son dernier jour).modelUsage: {}et omettaientdailyModelTokens/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 lemodifiedde la session).read-session-file.js—extractDailyMetrics()(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 purementtool_resultne sont pas comptés comme messages (évite le gonflement constaté à 116991 msg/jour).dailyMetricsattaché aux objets parent + subagent.db.js— migration v5 (session_metrics+ index date) ;replaceSessionMetrics()(delete-by-session puis insert, transactionnel) ; suppression câblée dansdeleteCachedSession/deleteCachedFolder(l'effacement par folder sous-sélectionnesession_cache, donc il passe en premier) ; agrégatsgetDailyMetrics/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.js—get-stats-from-db/refresh-statsconstruisent l'objet stats complet viabuildStatsFromDb().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.jsrelit tous les JSONL à chaque démarrage). La migration v5 ne purge volontairement passession_cache.Tests
test/read-session-file-metrics.test.js— unitextractDailyMetrics+ intégrationreadSessionFile.test/db-session-metrics.test.js— miroir pur-JS de l'agrégation SQL (better-sqlite3 ne charge pas sousnode: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èleclaude-opus-4-8).🤖 Generated with Claude Code