Skip to content

fix(file-browser): keep recursive paginated entries reachable (#345) - #357

Open
kimbotai1337 wants to merge 2 commits into
daggerhashimoto:nextfrom
kimbotai1337:fix/issue-345-file-browser-recursive-cursor
Open

fix(file-browser): keep recursive paginated entries reachable (#345)#357
kimbotai1337 wants to merge 2 commits into
daggerhashimoto:nextfrom
kimbotai1337:fix/issue-345-file-browser-recursive-cursor

Conversation

@kimbotai1337

@kimbotai1337 kimbotai1337 commented May 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #345.

Summary

CodeRabbit's review of #339 flagged three coupled issues in listDirectoryPage that, in combination, return paginated tree responses where some entries are silently dropped and unreachable via any subsequent cursor. The fix lands all three, plus regression tests against tiny budget fixtures so the failure modes are exercised directly.

The three issues, paraphrased from the CR review on #339:

  1. nextCursor was computed from start + pageItems.length before the recursive descent and file-listing loop. If the shared budget exhausted during recursion, dropped items had no way back.
  2. The shared budget was mutated across siblings and the parent/child boundary. A deep subtree could burn the budget under sibling dirN, after which dirN+1, ..., dirM were silently skipped but nextCursor had already advanced past them.
  3. Child listings reused the parent limit and only the entries of the recursive call were retained. Per-child truncated and nextCursor were discarded, so nested overflow was invisible to the caller.

What changed

  • nextCursor is computed at the return statement from start + dirsConsumed + filesConsumed. dirsConsumed is the count of dirs whose iteration completed (after the recursive call returned); filesConsumed is allowedFileCount when the dir loop fully drained, else 0.
  • The file-listing loop is gated on dirsConsumed === directories.length. If any sibling subtree exhausted the shared budget, files at this level are skipped and the cursor still points at the first unconsumed sibling.
  • TreeEntry gains two optional fields on directory entries: childrenTruncated?: boolean and childrenNextCursor?: string. Both omitted from the JSON when not meaningful (verified via not.toHaveProperty). Forward-compatible with existing consumers; frontend TreeEntry updated to match.
  • listDirectoryPage accepts an optional childLimit in its options bag (defaults to options.limit). No behavior change today; documented hook for future per-child paging tuning.
  • shouldIncludeDirent now filters non-file/non-directory Dirents (symlinks, sockets, FIFOs). Without this, the new dirsConsumed + filesConsumed count could undercount pageItems, point nextCursor at already-emitted items, and emit duplicates on the next page. The old code silently dropped these via the dirs/files split anyway; filtering at the gate keeps totalEntries and the consumed count consistent.
  • MAX_TREE_RESPONSE_ENTRIES is now overridable via config.fileBrowserMaxTreeEntries / FILE_BROWSER_MAX_TREE_ENTRIES. Default still 5,000. Lets the regression tests trigger budget exhaustion against tiny fixtures.

Test plan

  • npx vitest run server/routes/file-browser.test.ts - 65/65 pass, including 8 new tests targeting the three bugs from the issue (deep-tree-across-budget, leaf-level budget, page-boundary, empty-dir, child-truncation surfacing, child schema cleanliness when not truncated, depth=1 no-recursion, plus the original supersede regression).
  • npx vitest run - full suite 2113/2113 pass.
  • npx eslint server/routes/file-browser.ts server/routes/file-browser.test.ts server/lib/config.ts src/features/file-browser/types.ts - clean.
  • npx tsc -p config/tsconfig.server.json --noEmit - clean.
  • Manual smoke: dev server (Vite + Hono) starts, app shell renders, no console errors related to this change.

Follow-ups noted but not landed here

These were surfaced during review but deliberately not auto-applied; tracked here for visibility, suitable for a follow-up PR or issue.

  • P2 testing: no regression test for fs.readdir throwing on a subdirectory during recursive descent. Implementation handles it (the catch block returns an empty DirectoryListing and dirsConsumed still increments) but there's no test exercising it.
  • P2 testing: no test passes cursor past totalEntries to exercise the Math.min clamping branch.
  • P2 testing: the U2 test asserts childrenNextCursor is defined but not value-correct, and does not assert non-overlap between the first child page and the resumed page.
  • P2 ts: the test file declares a local PageResponse type that mirrors the route's DirectoryListing. If the route shape changes, tests stay green while silently asserting the wrong shape. Cleanup: export TreeEntry and DirectoryListing from the route and import them in the test.
  • P3 correctness: when a child's recursive listing returns 0 entries and truncated: true, the parent renders children: [] plus childrenTruncated: true. Clients that ignore childrenTruncated cannot distinguish this from a genuinely empty directory. Setting children: null in this corner case would make the API self-disambiguate.
  • Advisory: nextCursor semantics changed (bug fix, but technically observable). Clients caching cursor values across a server restart may land on a different page boundary on the next request. Worth a changelog line.

Notes

  • Base branch is next, not master. The code being fixed (listDirectoryPage) ships in PR perf: bound file browser tree rendering #339, which is on next but not yet on master.
  • CodeRabbit auto-review is disabled for next-targeted PRs. Triggered @coderabbitai full review manually after open.

@kimbotai1337

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kimbotai1337 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 23 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d778419-ab81-469b-b1c1-c0768aebb08b

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc0ad6 and 6bc5c9e.

📒 Files selected for processing (5)
  • server/lib/config.test.ts
  • server/lib/config.ts
  • server/routes/file-browser.test.ts
  • server/routes/file-browser.ts
  • src/features/file-browser/types.ts
📝 Walkthrough

Walkthrough

This PR implements a multi-part fix for recursive directory-tree pagination (issue #345). The shared response budget is now properly isolated per child, cursor advancement is deferred until after actual item consumption, and per-directory truncation metadata is exposed so clients can resume incomplete recursive listings.

Changes

File-browser recursive pagination fix

Layer / File(s) Summary
Design and problem statement
docs/plans/2026-05-22-001-fix-file-browser-recursive-pagination-plan.md
Planning document outlines three coupled bugs (early cursor advance, shared budget mutation, missing child truncation), high-level refactor strategy, three implementation units with regression scenarios, system impact, and risk analysis.
Type contracts and configuration
src/features/file-browser/types.ts, server/lib/config.ts
TreeEntry gains optional childrenTruncated and childrenNextCursor fields. config.fileBrowserMaxTreeEntries makes the shared response budget limit configurable via environment variable, defaulting to 5,000.
Core pagination logic refactor
server/routes/file-browser.ts
listDirectoryPage now tracks consumed directory and file items separately, computes nextCursor after processing completes (not before recursion), introduces per-child childLimit to isolate pagination within the shared budget, and conditionally surfaces childrenTruncated and childrenNextCursor on parent directory entries when child listings overflow. Helper getMaxTreeResponseEntries() replaces fixed budget constant.
Regression test coverage
server/routes/file-browser.test.ts
Test helper supports fileBrowserMaxTreeEntries configuration. Comprehensive new tests verify that siblings remain reachable across pages when recursion exhausts budget (issue #345), child pagination fields appear only on overflow, and depth=1 correctly omits child fields.
Residual code review findings
docs/residual-review-findings/fix-issue-345-file-browser-recursive-cursor.md
Documents unresolved findings: testing gaps for edge cases, TypeScript contract/test typing risks, correctness semantics questions, and advisory about observable nextCursor semantics shift for release notes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • daggerhashimoto/openclaw-nerve#339: Both PRs modify file-browser pagination and cursor/metadata handling in /api/files/tree endpoint; this PR's budget/cursor refactor is a targeted follow-up to the broader pagination implementation.

🐰 A rabbit hops through nested folders deep,
With budgets to track and cursors to keep!
Siblings no longer vanish mid-page,
Children can paginate—no more lost wage! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: keeping recursive paginated file-browser entries reachable, directly addressing issue #345.
Linked Issues check ✅ Passed All three core issues from #345 are addressed: nextCursor computed after processing, shared budget mutation prevented by gating file-listing loop, and child-level truncation surfaced via new TreeEntry fields.
Out of Scope Changes check ✅ Passed All changes directly support issue #345 objectives: config overridability for testing, test additions, documentation, type updates, and core pagination logic fixes are all in-scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description comprehensively documents the fix, changes, test plan, and follow-ups with clear rationale and implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 9736fc265d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/lib/config.ts Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/lib/config.ts (1)

82-84: ⚡ Quick win

Consider validating FILE_BROWSER_MAX_TREE_ENTRIES and warning on invalid values.

The parsing logic will silently coerce invalid values (e.g., "abc", "0") to 1 without feedback. While safe, this could confuse users attempting to configure the limit for testing scenarios described in the plan document.

✨ Suggested validation in `validateConfig()`

Add a validation block in the validateConfig() function (after line 312):

  // ── FILE_BROWSER_MAX_TREE_ENTRIES validation ───────────────────────────
  if (process.env.FILE_BROWSER_MAX_TREE_ENTRIES) {
    const parsed = Number(process.env.FILE_BROWSER_MAX_TREE_ENTRIES);
    if (Number.isNaN(parsed) || parsed < 1) {
      console.warn(
        `[config] ⚠ FILE_BROWSER_MAX_TREE_ENTRIES="${process.env.FILE_BROWSER_MAX_TREE_ENTRIES}" ` +
        `is invalid — using minimum value of 1`
      );
    }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/lib/config.ts` around lines 82 - 84, The FILE_BROWSER_MAX_TREE_ENTRIES
parsing silently coerces invalid inputs; add validation in validateConfig() to
detect when process.env.FILE_BROWSER_MAX_TREE_ENTRIES is present but not a
positive number and emit a console.warn (or processLogger.warn) showing the
original value and that a minimum of 1 (or the fallback) will be used; locate
the config export that sets fileBrowserMaxTreeEntries and add the suggested
Number(parsing) + Number.isNaN / < 1 check after that in validateConfig(),
logging a clear warning message including the raw environment string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@docs/residual-review-findings/fix-issue-345-file-browser-recursive-cursor.md`:
- Line 96: The docs currently include a machine-local path
`/tmp/compound-engineering/ce-code-review/20260522-091008-3574e918/` which is
inaccessible to other reviewers; replace that hardcoded `/tmp/...` entry in the
document with a reproducible reference (for example: a CI artifact URL, the
attached PR artifact name, or a repo-relative path under
docs/artifacts/<artifact-name>), and update any surrounding text to instruct
readers how to retrieve the artifact (e.g., “See CI artifact: <url>” or “See
artifact: reviews/20260522-3574e918.json”); ensure the symbol/string
`/tmp/compound-engineering/ce-code-review/...` is removed or replaced throughout
the file.

---

Nitpick comments:
In `@server/lib/config.ts`:
- Around line 82-84: The FILE_BROWSER_MAX_TREE_ENTRIES parsing silently coerces
invalid inputs; add validation in validateConfig() to detect when
process.env.FILE_BROWSER_MAX_TREE_ENTRIES is present but not a positive number
and emit a console.warn (or processLogger.warn) showing the original value and
that a minimum of 1 (or the fallback) will be used; locate the config export
that sets fileBrowserMaxTreeEntries and add the suggested Number(parsing) +
Number.isNaN / < 1 check after that in validateConfig(), logging a clear warning
message including the raw environment string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bea9b3d2-f9f5-4cf9-80c9-18b41e1b866e

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc0ad6 and 9736fc2.

📒 Files selected for processing (6)
  • docs/plans/2026-05-22-001-fix-file-browser-recursive-pagination-plan.md
  • docs/residual-review-findings/fix-issue-345-file-browser-recursive-cursor.md
  • server/lib/config.ts
  • server/routes/file-browser.test.ts
  • server/routes/file-browser.ts
  • src/features/file-browser/types.ts

Comment thread docs/residual-review-findings/fix-issue-345-file-browser-recursive-cursor.md Outdated
…rhashimoto#345)

CodeRabbit flagged three coupled issues in listDirectoryPage during review
of daggerhashimoto#339, surfaced as issue daggerhashimoto#345:

1. nextCursor advances past items the shared budget never materialized.
2. Shared budget mutation across siblings + recursion strands later
   siblings on the same page so a follow-up paginated request can't
   reach them.
3. Child directory listings reuse the parent limit with no per-child
   nextCursor surfaced, so nested overflow is invisible to the caller.

Refactor listDirectoryPage to:

- Compute nextCursor at the return statement from items actually consumed
  (dirsConsumed + filesConsumed), not from the slice length.
- Gate file-listing on full dir-loop completion at this level. If a
  sibling subtree exhausts the budget, files at this level are skipped
  so the cursor still points at the first unconsumed sibling.
- Surface child truncation on the directory TreeEntry via two new
  optional fields, childrenTruncated and childrenNextCursor. Omitted
  from the JSON when not meaningful. Frontend TreeEntry kept in sync.
- Add an explicit childLimit option (defaulting to options.limit) as
  the documented hook for future per-child paging tuning.
- Filter non-file/non-directory Dirents (symlinks, sockets, FIFOs)
  at shouldIncludeDirent so totalEntries and the consumed count
  stay in agreement under the new cursor math.

The shared MAX_TREE_RESPONSE_ENTRIES cap remains in force; this change
makes its interaction with pagination correct. Added an env override
(FILE_BROWSER_MAX_TREE_ENTRIES) so the regression tests can trigger
budget exhaustion against tiny fixtures.

Regression tests cover the three scenarios called out in the issue
(flat-dir-at-budget, deep-tree-across-budget, page-boundary-at-dir/file)
plus child-truncation surfacing, schema cleanliness when no truncation
occurs, depth=1 no-recursion semantics, and the rapid-supersede leak
this PR was originally cut to address.
@kimbotai1337
kimbotai1337 force-pushed the fix/issue-345-file-browser-recursive-cursor branch from 9736fc2 to 02185e2 Compare May 22, 2026 06:42
@kimbotai1337

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@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: 02185e2ce6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/lib/config.ts Outdated
…hashimoto#357)

Codex round 1 flagged two related issues in the env-var parse:

- A non-numeric value like FILE_BROWSER_MAX_TREE_ENTRIES=abc was being
  coerced via `Number(...) || 0` then clamped with `Math.max(1, ...)`
  to 1, silently dropping every tree response to a single entry instead
  of falling back to the documented 5,000 default.
- A fractional value like 2.5 was accepted as-is, letting filesConsumed
  become fractional and producing fractional nextCursor values that
  parseTreeCursor then floors, drifting paginated windows.

Replace the inline ternary with a small parsePositiveIntEnv helper that
requires a finite positive integer and falls back to the default
otherwise. Added regression tests covering unset / non-numeric /
fractional / zero / negative / valid-override.
@kimbotai1337

Copy link
Copy Markdown
Collaborator Author

Round 1 - pushed 6bc5c9e:

P1 fixed (3):

  • server/lib/config.ts:83-84 Codex flagged FILE_BROWSER_MAX_TREE_ENTRIES parsing twice (invalid values clamped to 1 instead of 5000 default; fractional values leaked into budget.remaining as floats). Replaced Math.max(1, Number(env) || 0) with a parsePositiveIntEnv helper that requires a finite positive integer and falls back to the 5000 default otherwise. Both threads resolved.
  • docs/residual-review-findings/... CR Minor: machine-local /tmp/... path in committed docs. Resolved by deletion in the prior force-push (residual findings are now inlined in the PR body).

Regression tests added in server/lib/config.test.ts: unset / non-numeric / fractional / zero / negative / valid-override.

Validation:

  • npx vitest run server/lib/config.test.ts server/routes/file-browser.test.ts -> 114/114 pass
  • npx eslint server/lib/config.ts server/lib/config.test.ts server/routes/file-browser.ts -> clean
  • npx tsc -p config/tsconfig.server.json --noEmit -> clean

I argue with the rabbits so you can ship
posted by Kimbot1337

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

@kimbotai1337 Nice work on the parsePositiveIntEnv helper — the original Math.max(1, Number(env) || 0) had two real defects you correctly identified:

  1. Fractional pass-through: Number("1.5") is truthy and non-zero, so || 0 would never activate; 1.5 would propagate into budget.remaining and cause off-by-one drift in page boundaries.
  2. Clamp-to-1 instead of default: Any invalid string like "abc" produces NaN, NaN || 00, then Math.max(1, 0)1, silently capping the tree budget at 1 entry instead of falling back to 5,000.

The parsePositiveIntEnv helper (finite positive integer check → fallback to 5,000) cleanly fixes both. The regression suite covering unset / non-numeric / fractional / zero / negative / valid-override is exactly the right set of cases. Triggering a full review of the updated changes now.

🐇 ✅

✅ Actions performed

Full review triggered.

@kimbotai1337

Copy link
Copy Markdown
Collaborator Author

Bot cycle complete. No remaining P0/P1 findings after 1 round on this branch. Ready for human review.

speaks bot, ships code
posted by Kimbot1337

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.

2 participants