Skip to content

fix: bound session history load to a tail window (fixes #509, #555) - #587

Open
Nuctori wants to merge 5 commits into
agegr:mainfrom
Nuctori:fix/session-history-tail
Open

fix: bound session history load to a tail window (fixes #509, #555)#587
Nuctori wants to merge 5 commits into
agegr:mainfrom
Nuctori:fix/session-history-tail

Conversation

@Nuctori

@Nuctori Nuctori commented Aug 23, 2026

Copy link
Copy Markdown

Summary

Bounds how much of a session is loaded when a session detail is opened, so deep/long sessions no longer crash or stall.

  • Feature request: conversation management & chat archive (对话管理和对话存档) #509 (stack overflow / HTTP 500 on open): BranchNavigator walked the active path with recursion over an arbitrarily deep chain; rewritten as explicit-stack iteration. lib/session-reader already slices the chain, so the SDK recursion that previously overflowed is no longer on the open path.
  • Performance: page session detail history instead of returning the full active branch #555 (full-history transfer / slow open): detail + context APIs returned the entire entry forest. They now return only a tail window of the active branch, and support upward pagination via ?before, so opening a 5000-message linear session transfers ~50 messages instead of 5000.
  • extra crash found via real-browser E2E: long sessions with a string assistant content (not a block array) hit TypeError: message.content.map is not a function in entryToUiMessage. Guarded with an Array.isArray check.
  • Codex review fixes (P1/P2 on the earlier branch state): restore multi-root branch detection in hasBranch (iterative rewrite had dropped the nodes.length > 1 check); expand the rendered window so prepended (older) pages stay visible and the "load earlier" sentinel shows when the initial tail is a truncation.

Reproduce

A session with >3000 messages and no branches (a single linear chain). Open it:

  • before: GET /api/sessions/:idHTTP 500. List page is fine.
  • after: detail returns 200 with context.messages.length === 50; scrolling up fetches ?before=<oldest> pages without duplicating the boundary.

Changed

  • lib/session-reader.ts: sliceActiveBranch (+excludeLeaf for page dedupe), buildSessionContext gains tail/excludeLeaf; entryToUiMessage guards string content.
  • app/api/sessions/[id]/route.ts, app/api/sessions/[id]/context/route.ts: ?tail (default 50, capped 1000, NaN-safe) and ?before pagination params.
  • hooks/useAgentSession.ts: loadContext(sid, leafId, before?) prepends older pages.
  • components/ChatWindow.tsx: top sentinel triggers loadContext for earlier history; rendered window expands to keep prepended pages visible.
  • components/BranchNavigator.tsx: buildActivePath/hasBranch recursion → explicit stack (with multi-root check).
  • Tests: lib/session-reader.pagination.test.mjs, app/api/sessions/detail-route.test.mjs, app/api/sessions/context-route.test.mjs, components/BranchNavigator.test.mjs (6000-deep chain + multi-root).
  • package.json: version 0.8.9 → 0.8.10.

Verification

  • npm test: with the harness fix from test: fix component-test harness dual React instance (35 -> 9 failures) #588 applied, 604 tests / 592 pass / 9 fail. The 9 remaining failures are platform-conditional (Windows symlink / PATH separator) or pre-existing brittle source-structure assertions — no regression from this change.
  • tsc --noEmit clean; ESLint clean on changed files.
  • Real-browser E2E (5000-message linear session): detail 200 + 50 messages, ?before pages without duplicate, no console/page errors (run manually; the CI E2E job lives in the CI PR).

Note

Closes #509. Closes #555.

…egr#555)

- slice branch chain to a tail window in buildSessionContext; add ?tail/&before pagination to detail + context APIs

- BranchNavigator recursion -> iterative (deep linear chains no longer overflow the stack)

- guard entryToUiMessage against string assistant content (real long sessions 500'd on content.map)

- ChatWindow sentinel + useAgentSession.loadContext prepend older pages

- e2e (playwright) + pagination tests
… data layers

- detail + context route tests assert ?tail parsing (default 50, NaN-safe, capped 1000) and ?before excludeLeaf wiring

- session-reader pagination tests add excludeLeaf page dedupe, string-assistant-content guard, and large-tail chain

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9780da2228

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hooks/useAgentSession.ts
Comment on lines +537 to +540
if (before) {
// Older page: prepend so scroll position stays anchored.
setMessages((prev) => [...d.context.messages, ...prev]);
setEntryIds((prev) => [...d.context.entryIds, ...prev]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reveal prepended history pages

When before is set, this prepends the older page but ChatWindow still renders only getVisibleRenderWindow(rendered.length, visibleCount), with visibleCount left at the initial page size. For a long session, the prepended entries land before startIndex and are immediately sliced away, so scrolling up never exposes history older than the default tail (and an initial exactly-50-entry tail does not render the sentinel at all). The pagination path needs to expand/track the visible window or otherwise render the newly loaded page.

Useful? React with 👍 / 👎.

Comment on lines +107 to 110
const stack: SessionTreeNode[] = [...nodes];
while (stack.length > 0) {
const node = stack.pop()!;
if (node.children.length > 1) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve multiple root branches in the navigator

This iterative rewrite no longer treats nodes.length > 1 as a branch. Sessions branched from the first message have multiple root nodes (and selectTopLevelBranches still returns those roots), but hasBranch now returns false when each root has at most one child, causing the UI to show the no-branches state and hide the branch choices. Re-add the top-level nodes.length > 1 check before walking the stack.

Useful? React with 👍 / 👎.

Comment thread package.json Outdated
"katex": "^0.16.47",
"mammoth": "^1.12.0",
"mermaid": "^11.16.1",
"playwright": "^1.48.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the lockfile for Playwright

Adding playwright only to package.json leaves package-lock.json out of sync: the lockfile root still has no devDependencies.playwright entry and no node_modules/playwright package. Lockfile-based installs will not reproduce this dependency (or will fail while reconciling it), so the new top-level e2e scripts that import playwright cannot be run from a clean locked install. Please regenerate and commit the lockfile with this dependency and version bump.

Useful? React with 👍 / 👎.

@Nuctori

Nuctori commented Aug 23, 2026

Copy link
Copy Markdown
Author

Update: this branch now also fixes the component-test harness (see #586). React, react-dom/server, and the i18n provider are loaded through the same jiti instance the components use, and useI18n is imported via the @/ alias so I18nContext resolves to a single module — previously two React instances made every component test throw 'useI18n must be used inside I18nProvider'.

Suite: 603 tests, 591 pass, 9 fail (was 565/35). Remaining 9 = platform-conditional (symlink, Windows PATH separator) + pre-existing brittle source-structure assertions, not regressions. No product code changed by the harness fix.

@Nuctori
Nuctori force-pushed the fix/session-history-tail branch from b028e5f to 9780da2 Compare August 23, 2026 19:58
@Nuctori

Nuctori commented Aug 23, 2026

Copy link
Copy Markdown
Author

Split out the test-harness fix into its own PR (#588) to keep this one scoped to #509/#555. Reverted the harness commit from this branch.

…isibility, drop playwright dep)

- BranchNavigator.hasBranch: restore the multiple-root check (nodes.length > 1) that the iterative rewrite dropped; sessions branched from the first message have multiple roots and were misreported as branchless.

- ChatWindow: expand the rendered window to at least the loaded message count so prepended (older) pages stay visible instead of being sliced off the top, and show the 'load earlier' sentinel when the window is full (the initial tail is a truncation).

- package.json: remove the playwright devDependency that was added for local E2E probing; it does not belong to this fix and left package-lock.json out of sync.

- test: cover hasBranch for multiple-root trees.
@Nuctori

Nuctori commented Aug 23, 2026

Copy link
Copy Markdown
Author

Addressed all three P2 items in 7c9b27f:

  1. Reveal prepended history pages — fixed. ChatWindow now keeps the rendered window at least as large as the loaded message count (Math.max(visibleCount, messages.length)), so prepended older pages are no longer sliced off the top. Also shows the 'load earlier' sentinel when the window is full (the initial tail is a truncation), so the first page can always be fetched.
  2. Preserve multiple root branches — fixed. hasBranch iterative rewrite had dropped the
    odes.length > 1 check; restored it. Sessions branched from the first message (multiple roots) are now correctly reported as branched. Added a test for this case.
  3. Lockfile for Playwright — removed. playwright was only added locally for E2E probing and does not belong to this fix; dropped it from package.json (it was never in the upstream lockfile, so there is nothing to regenerate).

…ripts from the PR

- a previous edit accidentally replaced the mermaid devDependency with a duplicate postcss entry; restored mermaid and removed the duplicate.

- e2e_browser.mjs/e2e_click.mjs/e2e_trace.mjs were temporary local probes and should not be in this PR; removed from the tree (kept locally, gitignored).
@Nuctori

Nuctori commented Aug 23, 2026

Copy link
Copy Markdown
Author

PR cleaned up: restored the mermaid devDependency (a previous edit had accidentally replaced it with a duplicate postcss entry) and dropped the temporary local e2e probe scripts (�2e_browser.mjs etc.) from the tree. Body now states the dependency on #588 for the component tests to pass on a clean checkout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant