Skip to content

fix(#54): recompute context stats on in-place session refresh - #57

Merged
axisrow merged 2 commits into
mainfrom
fix/54-refresh-context-stats
Oct 7, 2026
Merged

axisrow merged 2 commits into
mainfrom
fix/54-refresh-context-stats

Conversation

@axisrow

@axisrow axisrow commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Проблема

refreshSessionInPlace заменял conversation при дописывании JSONL, но не запускал
пересчёт контекстной статистики (Phase 2, processSessionContextWithPhases).
sessionContextStats/sessionPhaseInfo замерзали на fetch-овых значениях; плашка
Context через findLastTrackedAiGroupId вечно указывала на последнюю старую
группу — «Context 45k / $0.80» мог провисеть до конца сессии. Заморожена была и
стоимость хода — та же причина. Обе части закрываются одним фиксом.

Фикс

Phase-2 блок извлечён из fetchSessionDetail в общий
runContextPhase2(set, get, { items, projectRoot, tabIds, isStale })
(один источник истины), и refresh-путь теперь его вызывает.

Race-защита — module-level epoch sessionPhase2Generation: каждый запуск
пересчёта инкрементирует его, in-flight запуски сбрасывают результат перед
каждой записью в store. Epoch взаимно инвалидирует fetch↔fetch, fetch↔refresh
и refresh↔refresh; без него stale-запуск мог бы записать статистику предыдущей
сессии поверх другой (этот сценарий пинится тестом). Отдельный дебаунс не
нужен: watcher уже троттлит (150мс-1с адаптивно), coalescing оставляет ≤1
refresh в полёте, а оба fast-path (unchanged-сентинел, chunk-fingerprint)
выходят до пересчёта.

SSH-режим скипается тем же гвардом, что и раньше.

TDD-доказательство

Новый test/renderer/store/sessionDetailSlice.test.ts (4 теста). До фикса оба
refresh-теста падали с sessionContextStats.has('c-ai-2') === false — статистика
заморожена, ровно симптом ишью. После — зелёные. Мутационные проверки (руками,
TS-мутирующего тула нет):

  • убрать вызов из refresh → падают 3 теста;
  • убрать per-tab запись → падает per-tab тест;
  • убрать epoch-гвард → падает race-тест (stale-запуск протекает в чужую карту).

Fixes #54

🤖 Generated with Claude Code

axisrow and others added 2 commits October 7, 2026 16:03
refreshSessionInPlace replaced the conversation but never recomputed
context stats, so the Context pill (findLastTrackedAiGroupId) and per-turn
cost stayed frozen at fetch-time values for the rest of a long session.

Extract the Phase-2 block from fetchSessionDetail into a shared
runContextPhase2 and call it from refreshSessionInPlace as well, guarded
by a module-level Phase-2 epoch (sessionPhase2Generation) that mutually
invalidates fetch/refresh computations so stats are never written in
reverse conversation order.

TDD: new sessionDetailSlice.test.ts (4 tests). Pre-fix, the two refresh
tests failed with sessionContextStats.has('new-group') === false — stats
frozen at fetch values, exactly the issue symptom. Post-fix: green. All
three mutants killed: dropping the refresh call fails 3 tests; dropping
per-tab writes fails the per-tab test; dropping the epoch guard fails
the stale-run race test (stale run would leak the previous session's
group into the other session's stats map).

Fixes #54

Co-Authored-By: Claude Code <noreply@anthropic.com>
…erations

Code-review of the first cut found a class of invalidation bugs: Phase-2
stats are per-session but stored in unkeyed fields, and generation-based
guards kill or admit runs by the wrong criterion.

Replace the Phase-2 epoch counter and the refresh-generation guard with
write-time identity checks: a run commits only while the live conversation
still IS the items array it computed over (global) and each target tab
still shows that same conversation (per-tab, now one merged set()).

- a no-op refresh (unchanged sentinel / fingerprint skip) no longer
  abandons the in-flight Phase-2 of the last commit — that left the
  just-committed conversation with stale stats forever (pinned by a new
  test with a deferred IPC mock and an `unchanged` sentinel response);
- a Phase-2 racing a switch to another session (cached-tab swap or full
  fetch) drops its result instead of writing the old session's stats
  over it (race test retargeted from the epoch to the identity guard);
- ssh guard hoisted into runContextPhase2; refresh no longer skips
  Phase-2 for an empty-items conversation (matches fetch semantics);
- tests: drop dead TURN3 fixture and a leftover console.log, give each
  fixture session its own projectPath (module-level agent-configs cache
  is shared across tests).

Mutations killed: removing the identity guard fails the race test
(stale run leaks the previous session's group into the other session's
map); restoring the refresh-generation guard fails the no-op test.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow
axisrow merged commit e6edb13 into main Oct 7, 2026
1 check passed
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.

Context stats are never recomputed on in-place refresh — pill staleness is unbounded

1 participant