diff options
| author | Adam Malczewski <[email protected]> | 2026-06-22 12:24:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-22 12:24:43 +0900 |
| commit | 5066aad92282068dfd3ce5e1bf87164264618cfe (patch) | |
| tree | bfdf4fd31dbb92ccaf0eceb6cd49d254d252694a | |
| parent | d7d3ac7a20815b1890ee7142dcc51e5e6d2dda03 (diff) | |
| download | dispatch-web-5066aad92282068dfd3ce5e1bf87164264618cfe.tar.gz dispatch-web-5066aad92282068dfd3ce5e1bf87164264618cfe.zip | |
fix(metrics): tail-align when stepId matching fails — prevent misaligned turns
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.
| -rw-r--r-- | src/core/metrics/place.ts | 45 |
1 files 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++; } } |
