From 5066aad92282068dfd3ce5e1bf87164264618cfe Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 22 Jun 2026 12:24:43 +0900 Subject: fix(metrics): tail-align when stepId matching fails — prevent misaligned turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When stepIds are absent on persisted chunks (or don't match), the sequential fallback was HEAD-aligning — matching the OLDEST entries (trimmed turns) to the NEWEST segments. This showed 'turn 1' on turn 20's content and placed the wrong metrics on the wrong segments. Fix: when stepId matching produces ZERO matches, use TAIL-ALIGNMENT instead — match the LAST T entries to the T segments (the loaded chunks are always the newest). The oldest entries (trimmed turns) are unmatched and emit standalone turn-metrics rows at the top. 686 tests green. --- src/core/metrics/place.ts | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/core/metrics/place.ts b/src/core/metrics/place.ts index b60c675..47d3c5f 100644 --- a/src/core/metrics/place.ts +++ b/src/core/metrics/place.ts @@ -120,18 +120,41 @@ export function interleaveTurnMetrics( } // Pass 2: sequential fallback for unmatched segments. - let nextUnused = 0; - for (let seg = 0; seg < T; seg++) { - if (segmentEntry.has(seg)) continue; - while (nextUnused < K && usedEntries.has(nextUnused)) nextUnused++; - if (nextUnused < K) { - usedEntries.add(nextUnused); - const e = entries[nextUnused]; - if (e !== undefined) { - segmentEntry.set(seg, e); - segmentEntryIndex.set(seg, nextUnused); + // If NO segments were matched by stepId (pass 1), use TAIL-ALIGNMENT: + // the loaded chunks are always the NEWEST (chat-limit/windowing keeps the + // newest and trims the oldest), so match the LAST T entries to the T + // segments. This prevents misaligning oldest (trimmed) entries to newest + // segments — which would show "turn 1" on turn 20's content. + const pass1Matches = segmentEntry.size; + if (pass1Matches === 0 && K >= T) { + // Tail-align: skip the first K-T entries (trimmed turns). + for (let seg = 0; seg < T; seg++) { + if (segmentEntry.has(seg)) continue; + const entryIdx = K - T + seg; + if (entryIdx < K && !usedEntries.has(entryIdx)) { + usedEntries.add(entryIdx); + const e = entries[entryIdx]; + if (e !== undefined) { + segmentEntry.set(seg, e); + segmentEntryIndex.set(seg, entryIdx); + } + } + } + } else { + // Head-align fallback for remaining unmatched segments. + let nextUnused = 0; + for (let seg = 0; seg < T; seg++) { + if (segmentEntry.has(seg)) continue; + while (nextUnused < K && usedEntries.has(nextUnused)) nextUnused++; + if (nextUnused < K) { + usedEntries.add(nextUnused); + const e = entries[nextUnused]; + if (e !== undefined) { + segmentEntry.set(seg, e); + segmentEntryIndex.set(seg, nextUnused); + } + nextUnused++; } - nextUnused++; } } -- cgit v1.2.3