Skip to content

Commit 8fc21af

Browse files
lis186Justin Leeclaude
authored
fix(client): cold replay child session + project migration recompute edge cases (#315)
* fix(client): cold replay child session + project migration recompute edge cases Bug 1: child sessions created during cold activation lacked _cold flag, hitting the hot recompute path per-entry (O(n²)). Gate on _coldActivating. Bug 2: post-activation only recomputed the activated session's project. Now recomputes child sessions and all projects (covers cwd migration). Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(client): use replay-seen sids instead of parentSessionId for cold child recompute parentSessionId is null on mergeColdSessions-created entries, so the previous parentSessionId === id check missed them entirely. Collect session IDs from replay entries directly, clear _cold on all of them. Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(client): try/finally for _coldActivating + re-render child session cards Codex review P2 fixes: - Wrap replay loop in try/finally so _coldActivating resets even if addEntry throws (prevents permanent suppression of live recompute). - Re-render child session cards after recompute so their displayed counts/costs reflect the corrected stats, not the seeded doubles. Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(client): zero child session stats before cold replay to prevent displayNum inflation mergeColdSessions seeds child sessions with count/cost from the session index. Without zeroing before replay, addEntry increments on top of the seeded values — displayNum starts at N+1 instead of 1, and final counts double. Zero on first encounter in the replay loop, same as the parent. Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(client): extract zeroSessionStats to eliminate triplicated stat-field list Code review finding: the 10-field stat zeroing block was duplicated at parent and child zero sites. Adding a stat field would require edits at 3 places (shotgun surgery risk). Single helper, called from both. Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(client): use zeroSessionStats in recomputeSessionStats too Round-2 code review caught the 4th inline copy of the 10-field stat zeroing in recomputeSessionStats. Same helper, single source of truth. Fixes #314 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Justin Lee <justinlee@91app.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 67bd590 commit 8fc21af

2 files changed

Lines changed: 43 additions & 13 deletions

File tree

public/entry-rendering.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ function addEntry(e) {
635635
// Live (hot): full recompute from allEntries (idempotent, O(n) per entry).
636636
// Batch: increment counts for displayNum; defer full recompute to post-batch.
637637
// Cold: increment all stats (entries not in allEntries; full recompute on activation).
638-
if (!_loading && !sess._cold) {
638+
if (!_loading && !sess._cold && !window._coldActivating) {
639639
recomputeSessionStats(sid);
640640
recomputeProjectCost(projName);
641641
if (prevProjectName && prevProjectName !== projName) recomputeProjectCost(prevProjectName);
@@ -919,9 +919,7 @@ function addEntry(e) {
919919
function recomputeSessionStats(sid) {
920920
const sess = sessionsMap.get(sid);
921921
if (!sess) return;
922-
sess.count = 0; sess.mainCount = 0; sess.subCount = 0; sess.retryCount = 0;
923-
sess.totalCost = 0; sess.inputTokens = 0; sess.outputTokens = 0;
924-
sess.toolCalls = {}; sess.toolCallTurns = 0; sess.toolFailTurns = 0;
922+
zeroSessionStats(sess);
925923
for (var i = 0; i < allEntries.length; i++) {
926924
var en = allEntries[i];
927925
if (en.sessionId !== sid) continue;

public/miller-columns.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,12 @@ function formatCurrentStepBreadcrumb() {
21662166
return 'step #' + (step.stepIdx + 1) + subLabel;
21672167
}
21682168

2169+
function zeroSessionStats(s) {
2170+
s.count = 0; s.mainCount = 0; s.subCount = 0; s.retryCount = 0;
2171+
s.totalCost = 0; s.inputTokens = 0; s.outputTokens = 0;
2172+
s.toolCalls = {}; s.toolCallTurns = 0; s.toolFailTurns = 0;
2173+
}
2174+
21692175
function selectSession(id) {
21702176
setFocus('sessions');
21712177
if (id === selectedSessionId) return;
@@ -2208,23 +2214,49 @@ function selectSession(id) {
22082214
// #308: zero stats before replay — sessions.json seeded them; addEntry
22092215
// increments on top (cold path), so without zeroing = double-count.
22102216
// Replay with _cold still true → incremental counts, not O(n²) recompute.
2211-
sess.count = 0; sess.mainCount = 0; sess.subCount = 0; sess.retryCount = 0;
2212-
sess.totalCost = 0; sess.inputTokens = 0; sess.outputTokens = 0;
2213-
sess.toolCalls = {}; sess.toolCallTurns = 0; sess.toolFailTurns = 0;
2217+
zeroSessionStats(sess);
22142218
const entries = data.entries || [];
2219+
// Track all sessions touched during replay (child sessions, migrations)
2220+
const replaySids = new Set([id]);
22152221
window._coldActivating = true;
2216-
for (const e of entries) addEntry(e);
2217-
window._coldActivating = false;
2218-
sess._cold = false;
2219-
if (typeof recomputeSessionStats === 'function') recomputeSessionStats(id);
2220-
const projName = getProjectName(sess.cwd);
2221-
if (typeof recomputeProjectCost === 'function') recomputeProjectCost(projName);
2222+
try {
2223+
for (const e of entries) {
2224+
if (e.sessionId && !replaySids.has(e.sessionId)) {
2225+
replaySids.add(e.sessionId);
2226+
// Zero child session stats seeded by mergeColdSessions (same reason as parent)
2227+
const cs = sessionsMap.get(e.sessionId);
2228+
if (cs && cs._cold) zeroSessionStats(cs);
2229+
}
2230+
addEntry(e);
2231+
}
2232+
} finally {
2233+
window._coldActivating = false;
2234+
}
2235+
// Clear _cold for all replayed sessions (parent + children)
2236+
for (const sid of replaySids) {
2237+
const s = sessionsMap.get(sid);
2238+
if (s) s._cold = false;
2239+
}
2240+
if (typeof recomputeSessionStats === 'function') {
2241+
for (const sid of replaySids) recomputeSessionStats(sid);
2242+
}
2243+
// Recompute all projects — cwd migration means old project needs update too
2244+
if (typeof recomputeProjectCost === 'function') {
2245+
for (const [name] of projectsMap) recomputeProjectCost(name);
2246+
}
22222247
if (data.sessionTitles) {
22232248
for (const [sid, title] of Object.entries(data.sessionTitles)) {
22242249
const s = sessionsMap.get(sid);
22252250
if (s && !s.title) s.title = title;
22262251
}
22272252
}
2253+
// Re-render child session cards to reflect recomputed stats
2254+
for (const sid of replaySids) {
2255+
if (sid === id) continue;
2256+
const childEl = document.getElementById('sess-' + sid.slice(0, 8));
2257+
const childSess = sessionsMap.get(sid);
2258+
if (childEl && childSess) childEl.innerHTML = renderSessionItem(childSess, sid, childEl);
2259+
}
22282260
// Render — use _renderSelectedSession to avoid selectSession's id===selected guard
22292261
if (spinner.parentNode) spinner.remove();
22302262
_renderSelectedSession(id);

0 commit comments

Comments
 (0)