summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-22 12:24:43 +0900
committerAdam Malczewski <[email protected]>2026-06-22 12:24:43 +0900
commit5066aad92282068dfd3ce5e1bf87164264618cfe (patch)
treebfdf4fd31dbb92ccaf0eceb6cd49d254d252694a
parentd7d3ac7a20815b1890ee7142dcc51e5e6d2dda03 (diff)
downloaddispatch-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.ts45
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++;
}
}