Skip to content

Active time differs between analyze:session and analyze:sessions on the same file #91

Description

@axisrow

⚠️ Выводы не проверены человеком — доверия к ним нет. Ишью заведено по аудиту, который сделал Claude Code. Автор репозитория просил считать эти выводы непроверенными: прежде чем чинить, дважды воспроизведите каждый пункт по шагам ниже — на своих сессиях, а не по цифрам отсюда — и отметьте результат в разделе «Перепроверка». Claude перепроверил их один раз (одна сессия, замороженная копия файла); это не засчитывается.

Наблюдение (одна сессия, один и тот же файл)

analyze:session analyze:sessions
токены 14 967 189 14 967 189 ✓
активное время 22 мин (сумма округлённых по ходам; без округления 23,07) 18,54 мин
самый длинный ход 12 мин 11,3 мин

Комментарий в src/cli/sessionInventory.ts (~строка 233) утверждает «same accounting as turnActiveMinutes».

Вероятная причина (гипотеза)

  • buildLedger берёт одну строку на запрос — последнюю (keep-last) — и меряет промежутки последняя→последняя: время генерации ответа входит.
  • Инвентарь пропускает повторные строки запроса (snapshot) и меряет промежуток до первой строки запроса: время генерации не входит.
  • Сумма (последняя − первая строка) внутри запросов = 4,81 мин; разница между командами = 4,53 мин. Остаток 0,28 мин не объяснён.
  • Дополнительно analyze:session округляет каждый ход отдельно (Math.round в turnActiveMinutes) и суммирует округлённое.

Как воспроизвести

  1. Скопировать файл сессии (живая сессия растёт между запусками).
  2. pnpm analyze:session <copy> --json → .ledger.activeMinutes, .ledger.totals.longestTurn.
  3. Положить копию в $HOME/.claude/projects/<dir>/ временного HOME и запустить HOME=<tmp> pnpm analyze:sessions --json → activeMs, longestTurnMs.
  4. Посчитать Σ(последняя − первая строка) по requestId в главной цепочке.

Что сделать

  1. Выбрать одно определение активного времени (рекомендация: от первой строки запроса до последней — генерация тоже работа API) и вынести в общую функцию для обеих команд (и для хука, если он считает время).
  2. Округлять только итог, не каждый ход.
  3. Исправить комментарий в sessionInventory.ts.

Перепроверка (каждый пункт — дважды, независимо)

  • Время расходится на одном файле — проверка №1
  • то же — проверка №2 (другая сессия)
  • Расхождение объясняется точкой отсчёта (первая vs последняя строка запроса) — проверка №1
  • то же — проверка №2; объяснён ли остаток

Acceptance

  • Обе команды выдают одинаковое активное время и самый длинный ход на одном файле (тест на фикстуре)
  • Определение задокументировано в README

Activity

  1. axisrow commented on Oct 11, 2026

    @axisrow
    OwnerAuthor

    Дополнение по второй сессии: тот же Claude Code 2.1.296, другая сессия (аудит 2026-10-11). Это проверка Claude — она не засчитывается.

    analyze:session analyze:sessions
    токены 35 494 765 35 494 765 ✓
    активное время 27 мин (без округления 26,69) 22,46 мин
    самый длинный ход 20 мин 16,74 мин
    Как проверить на своей сессии (один раз, ~1 мин, из корня репозитория)
    cat > /tmp/repro-91.mts <<'EOF'
    import { readFileSync } from 'node:fs';
    const R = process.cwd(); // запускать из корня репозитория
    const F = process.argv[2]; // путь к копии JSONL-сессии
    const jsonl = await import(`${R}/src/main/utils/jsonl.ts`);
    const cli = await import(`${R}/src/cli/analyzeSession.ts`);
    const inv = await import(`${R}/src/cli/sessionInventory.ts`);
    const ledger = cli.buildLedger(await jsonl.parseJsonlFile(F));
    const CAP = 10 * 60_000;
    let raw = 0; // те же промежутки, что turnActiveMinutes, но без округления по ходам
    const byTurn = new Map<number, number[]>();
    for (const r of ledger.rounds) { const a = byTurn.get(r.turnIndex) ?? []; a.push(r.timestamp.getTime()); byTurn.set(r.turnIndex, a); }
    for (const ts of byTurn.values()) for (let i = 1; i < ts.length; i++) raw += Math.min(Math.max(ts[i] - ts[i - 1], 0), CAP);
    const first = new Map<string, number>(), last = new Map<string, number>(); // Σ(последняя − первая строка запроса)
    for (const l of readFileSync(F, 'utf8').split('\n')) {
      if (!l.trim()) continue;
      const e = JSON.parse(l);
      if (e.type !== 'assistant' || e.isSidechain || !e.requestId) continue;
      const t = Date.parse(e.timestamp);
      if (!first.has(e.requestId)) first.set(e.requestId, t);
      last.set(e.requestId, t);
    }
    let gen = 0;
    for (const [k, t0] of first) gen += last.get(k)! - t0;
    const r = await inv.scanSessionFile(F);
    const m = (ms: number) => +(ms / 60000).toFixed(2);
    console.table({
      'analyze:session, мин (сумма округлённых ходов)': ledger.activeMinutes,
      'analyze:session, мин (без округления)': m(raw),
      'analyze:sessions, мин': m(r.activeMs),
      'разница без округления, мин': m(raw - r.activeMs),
      'Σ(последняя − первая строка запроса), мин': m(gen),
    });
    EOF
    pnpm exec tsx /tmp/repro-91.mts ~/.claude/projects/<dir>/<session>.jsonl

    Если «разница без округления» почти равна «Σ(последняя − первая строка запроса)», гипотеза подтверждена. Живая сессия растёт между запусками, поэтому лучше дать путь к копии файла.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions