Skip to content

fix(core): avoid empty transcript history pages - #7582

Merged
ytahdn merged 1 commit into
QwenLM:mainfrom
chiga0:codex/fix-transcript-empty-history-page
Jul 23, 2026
Merged

fix(core): avoid empty transcript history pages#7582
ytahdn merged 1 commit into
QwenLM:mainfrom
chiga0:codex/fix-transcript-empty-history-page

Conversation

@ytahdn

@ytahdn ytahdn commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Backward transcript pagination now includes leading session metadata with the first conversation page when no earlier user turn exists. This prevents an otherwise empty metadata-only continuation page from being advertised as additional history.

Why it's needed

A persisted session can begin with an internal session-source record before its first user turn. Turn-aligned backward pagination previously left that record behind and reported more history, even though replaying the follow-up page produced no visible events. Clients consequently issued a redundant transcript request and observed historyHasMore: true followed by events: [].

Reviewer Test Plan

How to verify

Create a transcript whose active chain is session_source -> user -> assistant, then request a backward page with a limit larger than the transcript. Confirm the initial page includes the complete chain, reports hasMore: false, and has no continuation cursor. Also confirm ordinary multi-turn backward pages remain aligned to user turns.

Evidence (Before & After)

Before: the initial page omitted the leading session-source record and reported hasMore: true; the continuation page returned events: [] and hasMore: false.

After: the initial page consumes all 21 records in the reproduced session, reports hasMore: false, and returns no continuation cursor.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 22; targeted unit tests plus repository lint, build, and typecheck.

Risk & Scope

  • Main risk or tradeoff: Leading metadata may make the first backward page exceed its nominal record limit, matching the existing behavior for complete user turns; the byte budget is still enforced after expansion.
  • Not validated / out of scope: Archived transcripts and non-filesystem transcript implementations were not exercised end to end.
  • Breaking changes / migration notes: None.

Linked Issues

N/A

中文说明

本 PR 做了什么

当第一个用户轮次之前不存在更早的用户轮次时,向后加载 transcript 会把会话前置元数据并入第一个对话页。这样不会再把一个只能产生空结果的纯元数据续页标记为“还有历史”。

为什么需要

持久化会话可能在第一个用户轮次之前包含内部的 session-source 记录。此前按轮次对齐的向后分页会把该记录单独留在前一页,并报告还有历史,但续页 replay 后没有任何可展示事件。客户端因此会发出一次多余的 transcript 请求,并观察到 historyHasMore: true 后紧接着 events: []

Reviewer 测试计划

如何验证

创建 active chain 为 session_source -> user -> assistant 的 transcript,并使用大于 transcript 长度的 limit 请求向后分页。确认第一页包含完整链、返回 hasMore: false 且没有 continuation cursor;同时确认普通多轮会话的向后分页仍按用户轮次对齐。

修复前后证据

修复前:第一页遗漏前置 session-source 记录并返回 hasMore: true;续页返回 events: []hasMore: false

修复后:复现会话的第一页一次消费全部 21 条记录,返回 hasMore: false,且没有 continuation cursor。

测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows N/A
🐧 Linux N/A

环境

Node.js 22;运行了定点单元测试以及仓库 lint、build 和 typecheck。

风险与范围

  • 主要风险或取舍:前置元数据可能使第一个向后分页超过名义 record limit,这与完整用户轮次已有行为一致;扩展后仍会执行字节预算检查。
  • 未验证或范围外:未对归档 transcript 和非文件系统 transcript 实现执行端到端验证。
  • 破坏性变更或迁移说明:无。

关联 Issue

N/A

@ytahdn

ytahdn commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

E2E test report

Validated against an authorized local persisted session that reproduced the issue without inspecting or exposing conversation content.

  • Before: the first backward page returned 20 conversation records with hasMore: true; the continuation contained only system/session_source, projected to zero UI events, and returned hasMore: false.
  • After: the first backward page returned the complete 21-record active chain with hasMore: false and no continuation cursor.
  • The active chain was linear with no gaps or cycles.
  • The full transcript reader unit suite passed: 48/48.
  • Repository lint, build, and typecheck passed.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: Observed bug with clear evidence — backward pagination reports historyHasMore: true then the continuation page returns events: []. The before/after in the description demonstrates the issue concretely.

Direction: Aligned. Transcript pagination correctness is core session management — clients should never see a phantom "more history" signal followed by an empty page.

Size: 38 production lines (+26 −12) in packages/core/src/services/session-transcript-reader.ts, 26 test lines. Well within bounds; no maintainer escalation needed.

Approach: The scope feels right — one focused fix in selectBackwardPageUuids plus a targeted unit test. After aligning to the first user turn, the code now checks whether everything before it is leading metadata (no earlier turn start exists) and absorbs it into the current page. The byte-budget check is correctly preserved via the shared expandedSelection flag. No unrelated changes, no scope creep.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,有明确证据——向后分页报告 historyHasMore: true,但续页返回 events: []。PR 描述中的 before/after 具体展示了该问题。

方向:对齐。Transcript 分页正确性属于核心会话管理——客户端不应看到"还有历史"的信号后紧接着收到空页面。

规模:packages/core/src/services/session-transcript-reader.ts 中 38 行生产代码(+26 −12),26 行测试代码。远在阈值内,无需维护者关注。

方案:范围合理——selectBackwardPageUuids 中的一个聚焦修复加一个针对性单元测试。对齐到第一个用户轮次后,代码现在检查其前面是否全部为前置元数据(不存在更早的轮次起点),如果是则将其并入当前页。字节预算检查通过共享的 expandedSelection 标志正确保留。无无关改动,无范围蔓延。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 6ef176c5394187130fe007dfec12e80191702992 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: After aligning to the first replay turn boundary in selectBackwardPageUuids, check whether all records before selectedStart are non-turn-start metadata. If so, expand the selection to index 0 and re-check the byte budget.

PR's approach: Matches exactly. When alignedToReplayBoundary && selectedStart > 0, the code scans backward for an earlier turn start; finding none, it sets selectedStart = 0. The expandedSelection flag cleanly unifies the byte-budget re-check for both this new path and the existing !alignedToReplayBoundary path.

No correctness bugs, no security concerns, no regressions. The byte-budget enforcement is preserved correctly — the check that was previously inside the !alignedToReplayBoundary branch is now gated on expandedSelection, which covers both expansion paths without changing behavior for the non-expansion case.

The new unit test covers the exact scenario: session_source → user → assistant chain, backward page with limit > transcript size, asserting all records returned with hasMore: false.

Real-Scenario Testing

Exercised the SessionTranscriptReader API directly with a session_source → user → assistant transcript (tmux unavailable on this runner; used tsx scripts against main vs. PR code).

Before (main branch)

=== Transcript: session_source -> user -> assistant ===
Records written: source, u1, a1

=== Backward page (limit=100) ===
Records returned: [u1, a1]
hasMore: true
nextCursorState: {"v":1,"sessionId":"550e8400-e29b-41d4-a716-446655440000",...,"position":1,"direction":"backward",...}

⚠️  BUG: hasMore=true means client will request another page...

The session_source record is left behind at position 0. The page reports hasMore: true with position: 1, but the continuation page would only contain the non-visible metadata record — producing events: [].

After (this PR)

=== Transcript: session_source -> user -> assistant ===
Records written: source, u1, a1

=== Backward page (limit=100) — WITH FIX ===
Records returned: [source, u1, a1]
hasMore: false
nextCursorState: undefined

✅ CORRECT: hasMore=false, no phantom continuation page needed.

All records included in the first page, hasMore: false, no continuation cursor.

Unit tests

 ✓ src/services/session-transcript-reader.test.ts (48 tests) 103ms
 Test Files  1 passed (1)
      Tests  48 passed (48)

47 existing tests + 1 new test, all passing.

中文说明

代码审查

独立方案:selectBackwardPageUuids 中对齐到第一个 replay 轮次边界后,检查 selectedStart 之前的所有记录是否都是非轮次起点的元数据。如果是,将选择扩展到索引 0 并重新检查字节预算。

PR 方案: 完全一致。当 alignedToReplayBoundary && selectedStart > 0 时,代码向前扫描更早的轮次起点;找不到时将 selectedStart 设为 0。expandedSelection 标志干净地统一了两条扩展路径的字节预算重检。

无正确性 bug,无安全隐患,无回归。字节预算执行正确保留——之前在 !alignedToReplayBoundary 分支内的检查现在由 expandedSelection 控制,覆盖两条扩展路径而不改变非扩展情况的行为。

新单元测试覆盖了精确场景:session_source → user → assistant 链,limit 大于 transcript 长度的向后分页,断言返回所有记录且 hasMore: false

实际场景测试

直接使用 SessionTranscriptReader API 测试 session_source → user → assistant transcript(此运行器无 tmux;使用 tsx 脚本对比 main 与 PR 代码)。

修复前:session_source 被遗留在位置 0,页面报告 hasMore: true,续页只包含不可见的元数据记录。
修复后:所有记录包含在第一页,hasMore: false,无续页游标。

单元测试:48 个测试全部通过(47 个已有 + 1 个新增)。

Qwen Code · qwen3.8-max-preview

Reviewed at 6ef176c5394187130fe007dfec12e80191702992 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — Clean fix for a real pagination bug; reproduced before/after, minimal diff, all tests pass.

This is a textbook small bugfix. The problem is concrete and observable: backward transcript pagination leaves a leading session_source record behind, advertises hasMore: true, and the follow-up page comes back empty. The fix is the natural one — after aligning to the first user turn, check whether everything before it is leading metadata with no earlier turn boundary, and if so absorb it into the current page. The expandedSelection flag is a clean way to share the byte-budget re-check between the new path and the existing alignment path.

The diff is 26 production lines + 26 test lines, touches exactly one function and its test file, and carries no unrelated changes. The before/after reproduction confirms the fix works as described. 48 unit tests pass (47 existing + 1 new).

Nothing to second-guess here. Ships it. ✅

中文说明

置信度:5/5 — 对真实分页 bug 的干净修复;已复现 before/after,最小 diff,所有测试通过。

这是一个教科书式的小型 bugfix。问题具体且可观测:向后 transcript 分页遗留了前置 session_source 记录,报告 hasMore: true,续页返回空结果。修复方案自然——对齐到第一个用户轮次后,检查其前面是否全部为无更早轮次边界的前置元数据,如果是则并入当前页。expandedSelection 标志干净地在新路径和已有对齐路径之间共享字节预算重检。

Diff 为 26 行生产代码 + 26 行测试代码,仅触及一个函数及其测试文件,无无关改动。Before/after 复现确认修复如描述所述。48 个单元测试全部通过(47 个已有 + 1 个新增)。

无需犹豫。可以合并。✅

Qwen Code · qwen3.8-max-preview

Reviewed at 6ef176c5394187130fe007dfec12e80191702992 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

— qwen3.7-max via Qwen Code /review

maxBytes,
);
}
expandedSelection = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] expandedSelection = true is set unconditionally here, even when the while loop didn't move selectedStart (already aligned to a turn boundary). Functionally equivalent to prior behavior, but the name implies expansion occurred. Consider renaming to needsByteCheck or adding a comment: // always check bytes in the unaligned path, matching prior behavior.

— qwen3.8-max-preview via Qwen Code /review

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Correct fix — leading session metadata now folds into the first conversation page instead of producing an empty metadata-only continuation. maxBytes check covers both expansion paths. One inline nit on expandedSelection naming.

— qwen3.8-max-preview via Qwen Code /review

@ytahdn
ytahdn added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit e58f995 Jul 23, 2026
94 checks 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.

3 participants