review: address ds-review-bot findings on session-stats

- stateVersion starts at 1: the package is new, no persisted rows predate it
- pendingCalls pairs by own key so a provider callId naming a prototype
  property cannot fold toolMs to NaN on an unmatched crash-recovery result
- StatsLine folds the window fallback only when no sessionStats value is
  served, and gates the token group on actual token activity instead of
  steps, so failed-only sessions drop the zero-token group
- correct the crash-step counting semantics in the README and Agent Note:
  recovery closes interrupted steps with a synthetic step/end on reload
- reword the window-scoped alternative as a plain rejected option and name
  isTokenDelta's home beside the StreamChunk type
This commit is contained in:
07akioni
2026-08-12 21:43:35 +08:00
parent ae3f89e772
commit 62a437c082
20 changed files with 68 additions and 29 deletions
@@ -6,7 +6,7 @@ import { isTokenDelta } from '@deepseek-ai/dsh-llm/message'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type { AssistantTiming } from './conversation.ts'
// The first-token predicate lives with the StreamChunk vocabulary in dsh-llm;
// The first-token predicate lives beside the StreamChunk type in dsh-llm;
// re-exported here so Chat Definitions keep their client-runtime import.
export { isTokenDelta } from '@deepseek-ai/dsh-llm/message'
@@ -162,12 +162,13 @@ export interface StatsLineProps {
export const StatsLine = memo(function StatsLine({ useSession, useProjection, t }: StatsLineProps) {
const settledNodes = useSession(s => s.chat.legacy.nodes)
const windowStats = useMemo(() => deriveStats(settledNodes), [settledNodes])
const usage = useProjection('tokenUsage')
// Every figure rides the durable sessionStats projection, so paging and
// compaction cannot change any of them; an assembly without the unit falls
// back to the window-scoped fold wholesale (same field names).
const stats = useProjection('sessionStats') ?? windowStats
// back to the window-scoped fold wholesale (same field names), paid only
// while no projection value is served.
const projected = useProjection('sessionStats')
const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes])
// Pipe-separated groups (figma stats strip); a group with no data drops out whole.
const groups: string[] = []
if (stats.steps > 0) {
@@ -190,9 +191,11 @@ export const StatsLine = memo(function StatsLine({ useSession, useProjection, t
// Context occupancy deliberately lives on the composer's ContextMeter ring,
// not here — one home per fact.
// Billing rides the durable projection, so these survive paging and
// compaction. Suppress the empty projection on a brand-new session.
// compaction. Gated on actual token activity: a session whose steps all
// settled without billing (e.g. every request failed) shows its counts
// without a zero-token group.
if (usage !== undefined
&& (stats.steps > 0 || billedInputTokens(usage) > 0 || usage.outputTokens > 0)) {
&& (billedInputTokens(usage) > 0 || usage.outputTokens > 0)) {
const cacheHit = cacheHitPercent(usage)
if (cacheHit !== null) groups.push(t('stats.cacheHit', { percent: cacheHit }))
groups.push(t('stats.tokens', {
@@ -314,6 +314,17 @@ describe('StatsLine', () => {
expect(view.container.textContent).toBe('')
})
it('hides the zero-token group when steps closed without any billed activity', () => {
// A session whose only turn failed before billing (e.g. an auth error):
// the counts group renders alone, not an uninformative zero-token group.
const { source } = makeSource()
const view = render(<StatsLine {...props(source, {
tokenUsage: { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
sessionStats: sessionStats({ turns: 1, steps: 1 }),
})} />)
expect(view.container.textContent).toBe('1 turns · 1 steps')
})
it('keeps the counts group over an empty visible window when the projection carries totals', () => {
// Extends the durable-groups guarantee: full-session counts survive a
// window that compaction (or paging) left without assistant nodes.