Skip to content

fix: 仪表盘数字终于可信 — 时长、成本、日期、丢失的消息与 CLI 快捷方式#42

Merged
moose-lab merged 7 commits into
mainfrom
claude/review-phase3-numbers
Jun 12, 2026
Merged

fix: 仪表盘数字终于可信 — 时长、成本、日期、丢失的消息与 CLI 快捷方式#42
moose-lab merged 7 commits into
mainfrom
claude/review-phase3-numbers

Conversation

@moose-lab

Copy link
Copy Markdown
Owner

用户视角:这次更新你会感受到什么

DevLog 的核心价值是"如实告诉你今天干了什么、花了多少"。这批修复让这些数字第一次真正可信:

⏱️ 时长不再虚高一倍

每个带 turn_duration 的事件之前被计了两次——你看到的会话时长统计普遍偏高(最多 2 倍)。现在每段时长只计一次。

💰 「今日花费」恢复真实

  • 跨多天的长会话,花费记到每条消息实际发生的那天。 之前整个会话的全部成本一次性记在"最后活动日"——一个跑了一周的会话会让某天的花费暴涨几个数量级。
  • 成本记录现在带真实 token 数(输入/缓存/输出),不再是空占位,模型也按实际归属而非取第一个。

📅 凌晨的工作不再"消失"

报表按你的本地日历日分桶。之前日期键取的是 UTC:在东八区,凌晨 00:00–08:00 做的工作会落进前一天的日报,报表里的时间戳也早 8 小时。现在日报、周报、月报、HTML 导出全链路一个时区,CI 固定 TZ 的测试这次真的在守护这件事。

💬 旧会话里"凭空消失"的回复回来了

旧格式(纯字符串内容)的 assistant 消息之前解析出 0 条事件——翻看历史会话时整段回复缺失。现在两种角色、两种格式都正常显示。

📂 无时间戳会话的创建时间不再每次扫描都变

损坏或无时间戳的会话现在回退到文件创建时间,而不是"扫描那一刻"。

⌨️ CLI 快捷方式修复

  • devlog abc123(直接打开某个会话)和 devlog help 之前在 commander 14 下直接报错 "too many arguments",现在都正常。
  • devlog --version 显示真实版本号(之前停留在两个版本前的 0.4.0)。

范围与验证

  • 覆盖 review Phase 3「数字正确性」全部条目:IM-1(时长双计)、IM-2(旧格式消息)、IM-3(时区)、IM-4(firstActivity)、IM-24(成本归因),外加 IM-15(CLI 回退)与版本号同步。
  • 7 个独立提交,每个带回归测试(新增 TZ 边界、成本归因、解析器、CLI 接线测试);每次提交经 pre-commit 全量门禁;quality:build(Next + tsup)通过;CLI 行为用打包产物实测验证。
  • 旧缓存兼容:没有按日成本桶的历史扫描结果自动回退旧行为,不会出现成本丢失。

🤖 Generated with Claude Code

moose-lab and others added 7 commits June 11, 2026 17:58
IM-1: the turn_duration special case added durationMs, then the
generic durationMs branch added the same field again — every duration
stat the dashboard and reports show was inflated 2x for those events.
The generic read alone covers turn_duration system events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oles

IM-2: the string-content branch was nested inside the Array.isArray
branch (unreachable) and the fallback branch only handled the human
role, so legacy assistant messages with plain string content produced
zero events — entire turns missing from session views.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IM-4: firstActivity was initialized to new Date() (scan time), so
discovery's 'getTime() > 0' check was always true and the file
birthtime fallback never ran — sessions without timestamps got a
creation date that changed on every scan. A max-date sentinel now
tracks the minimum and normalizes to epoch when nothing was seen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IM-3: stored timestamps are SQLite UTC strings, but the day key was
sliced from the UTC text while range boundaries used local date
components — in Asia/Shanghai anything done 00:00-08:00 local landed
in the previous day's report, and rendered timestamps showed 8h early
(JS parses bare datetime strings as local, compounding the mismatch).

Date handling is now single-sourced in report-dates.ts: bare DB
timestamps parse as UTC, day keys and range boundaries both use the
local calendar, and the HTML renderer formats true local time. Adds
the TZ-boundary tests the CI TZ=Asia/Shanghai pin was meant for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IM-24: Claude cost was session-granular — one record per session
booked at last-activity time with empty token totals and a hardcoded
model, so a week-long session dumped its entire cost on one day and
'today's cost' could inflate by orders of magnitude. scanSession now
aggregates cost and tokens per (local day, model) into a compact
dailyUsage list, and the cost report emits one record per bucket with
real token counts. Sessions scanned before this change keep the
legacy whole-session fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ommander 14

IM-15: commander 14 hard-errors with 'too many arguments' for words it
doesn't recognize, so 'devlog abc123' (open a session) and 'devlog
help' were both broken in the published CLI. The default command now
declares an optional [ref] argument that routes to 'show', and 'help'
prints program help. Verified against the bundled dist/cli.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardcoded VERSION was two releases behind package.json (0.4.0 vs
0.5.0). The CLI now reads the version from the resolved package root,
so --version and the help banner always match the installed package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @moose-lab's task in 4m 2s —— View job


Code Review

  • Gather context (reading changed files)
  • Review correctness & logic
  • Review cross-platform / timezone safety
  • Review type safety & security
  • Review test coverage
  • Post findings

Findings

  • [should-fix] cache_creation_input_tokens silently merged into inputTokensparser.ts:113-114:

    bucket.inputTokens +=
      (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);

    SessionDailyUsage.inputTokens will contain cache-write tokens, so any dashboard display of per-day "input token" counts will be inflated whenever the model used prompt caching. Cost accuracy is unaffected (Claude costs flow from apiCostUSD, not token counts), but token-breakdown display is wrong. SessionDailyUsage has no cacheCreationInputTokens field, so the information is permanently lost once bucketed. Suggest either adding the field or at minimum documenting the semantic in the interface. Fix this →

  • [should-fix] Null-timestamp + epoch-lastActivity buckets cost to "1970-01-01"parser.ts:96-97:

    const when =
      ts ?? (meta.lastActivity.getTime() > 0 ? meta.lastActivity : new Date(0));

    If a legacy event has costUSD but no timestamp, and no earlier timestamped event has been seen yet, when becomes new Date(0)getTodayDateKey returns "1970-01-01". The bucket is created silently rather than skipped. In practice this requires a malformed session that has a cost event before any timestamped event, but the failure mode is invisible. Guard with early-return on new Date(0) or omit the bucket entirely. Fix this →

  • [nit] billingMode: "api" hardcoded in claudeSessionToRecordscost-tracker.ts:358. Pre-existing assumption in claudeSessionToRecord, now fan-outed to every per-day bucket. Subscription/Max users will have all records tagged "api". Not new, but worth tracking given the field exists.

  • [nit] replace(" ", "T") in parseDbTimestamp replaces only the first space (report-dates.ts:23). Correct for SQLite's datetime('now') format, but a double-space-corrupted string would silently produce a wrong parse. A comment noting the single-replacement is intentional would help future readers.


What's solid:

The core fixes are all correct and the reasoning checks out:

  • IM-1 (double duration): Removing the special turn_duration branch and keeping only the generic event.durationMs check is the right fix — the events have durationMs on them regardless of subtype, so the generic read already covers it.
  • IM-2 (lost legacy messages): Moving the role && typeof content === "string" branch to the outer else if (not nested inside the array path) correctly handles both roles in the legacy string-content format.
  • IM-3 (timezone): parseDbTimestamp appending Z to bare SQLite timestamps is correct — datetime('now') in SQLite produces UTC without a zone marker. The localDateKeygetTodayDateKey chain then correctly applies the local calendar day. Tests in report-dates.test.ts cover the UTC+8 boundary case.
  • IM-4 (firstActivity sentinel): Using new Date(8640000000000000) (JS max-date) as a min-tracking sentinel is correct. The === 8640000000000000 comparison is safe since that value is exactly representable as IEEE 754.
  • IM-24 (cost attribution): The per-(day,model) bucketing in the parser and the claudeSessionToRecords fan-out are correct. The legacy fallback path (!daily || daily.length === 0) preserves backward compat.
  • IM-15 (CLI): Declaring [ref] as an optional argument on the default command is the correct fix for commander 14's strict argument checking.
  • TZ test isolation: Pinning process.env.TZ = "Asia/Shanghai" at the top of both TZ-sensitive test files (before any imports) is the right approach — it must be set before any Date calls to take effect.

Verdict: APPROVE

The two should-fixes are real data-quality issues but neither affects cost correctness (Claude spend flows from apiCostUSD, not token-based repricing). All seven IM items are correctly addressed and backed by regression tests.

@moose-lab
moose-lab merged commit d84a6ea into main Jun 12, 2026
4 checks passed
@moose-lab
moose-lab deleted the claude/review-phase3-numbers branch June 12, 2026 03:44
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