Skip to content

fix(context): cap list_directory to 500 entries (GH-1809)#1827

Merged
RealKai42 merged 8 commits intomainfrom
n-WN/repro-token-limit-1809
Apr 10, 2026
Merged

fix(context): cap list_directory to 500 entries (GH-1809)#1827
RealKai42 merged 8 commits intomainfrom
n-WN/repro-token-limit-1809

Conversation

@n-WN
Copy link
Copy Markdown
Collaborator

@n-WN n-WN commented Apr 10, 2026

Summary

  • list_directory() 此前无条目上限,在包含数千文件的目录(如 node_modules)中启动 kimi 时,整个文件列表被注入系统提示词 KIMI_WORK_DIR_LS,轻松超过模型 262,144 token 上限
  • 新增 _LIST_DIR_MAX_ENTRIES = 500 截断阈值,超出后追加 ... and N more entries (use Glob or Shell to explore) 提示
  • 与代码库已有的截断模式保持一致(glob MAX_MATCHES=1000、git_context _MAX_DIRTY_FILES=20ToolResultBuilder max_chars=50000

Closes #1809

Test plan

  • 新增 test_list_directory_truncates_large_dirs 测试:创建 600 个文件,验证输出被截断为 500 行 + 1 行提示
  • 已有 test_list_directory_unix / test_list_directory_windows 测试不受影响(文件数远低于阈值)

Open with Devin

Summary

  • list_directory() previously had no upper limit for entries. When kimi is started in a directory containing thousands of files (such as node_modules), the entire file list is injected with the system prompt word KIMI_WORK_DIR_LS, easily exceeding the model's upper limit of 262,144 tokens.
  • Added _LIST_DIR_MAX_ENTRIES = 500 truncation threshold, append ... and N more entries (use Glob or Shell to explore) prompt after exceeding
  • Consistent with existing truncation patterns in the code base (glob MAX_MATCHES=1000, git_context _MAX_DIRTY_FILES=20, ToolResultBuilder max_chars=50000)

Closes #1809

Test plan

  • Added test_list_directory_truncates_large_dirs test: create 600 files, verify output is truncated to 500 lines + 1 line prompt
  • Existing test_list_directory_unix / test_list_directory_windows tests are not affected (number of files is well below the threshold)

Open with Devin

…en-limit blowup (#1809)

When the working directory contains thousands of files (e.g. node_modules),
list_directory() would enumerate every entry and inject the entire listing
into the system prompt via KIMI_WORK_DIR_LS, easily exceeding the model's
262,144-token context window.

Add _LIST_DIR_MAX_ENTRIES=500 cap with a trailing "... and N more entries"
hint, consistent with existing truncation patterns (glob MAX_MATCHES=1000,
git_context _MAX_DIRTY_FILES=20, ToolResultBuilder max_chars=50000).
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

n-WN added 2 commits April 10, 2026 17:14
Only top-level (single depth) entries are listed, so 200 is sufficient
for project overview while saving ~3.5K tokens of context budget.
Replace flat single-level ls output with a compact 2-level tree:

- Root level: up to 30 entries (dirs first, alphabetical)
- Child level: up to 10 entries per subdirectory
- Truncated levels show "... and N more" so the LLM knows more exists
- System prompt updated to explain tree is partial

This gives the model a much better project structure overview while
using far fewer tokens than a flat listing of hundreds of entries.
Worst-case output: ~240 lines ≈ 1.5K tokens (vs old 200-line flat
listing ≈ 2.4K tokens with less structural information).
Copy link
Copy Markdown

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

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: e129743420

ℹ️ 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 on lines +109 to +113
if is_dir:
lines.append(f"{connector}{name}/")
child_prefix = " " if is_last else "│ "
try:
child_entries, child_total = await _collect_entries(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent recursing into symlinked directories

This new tree expansion traverses any root entry marked as a directory, but entry.stat() in _collect_entries follows symlinks. If a workspace contains a top-level symlink to an external directory (for example /etc or another large path), list_directory() will now enumerate and inject out-of-workspace filenames into KIMI_WORK_DIR_LS, which is a data-scope regression compared to the previous non-recursive behavior. Guard recursion by skipping symlink dirs (or by checking canonical paths remain within work_dir) before calling _collect_entries on children.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Won't fix — analyzed and the impact is negligible:

  1. Depth is capped at 1 level with a width limit of 10 entries per subdirectory. A symlinked directory only exposes child names, not contents, and no further recursion occurs.
  2. No sandbox to bypass — the file tools (ReadFile, WriteFile, etc.) use canonical() which does not resolve symlinks, and is_within_workspace() is pure string matching. More importantly, these tools already allow reading/writing arbitrary files via absolute paths, so there is no security boundary that symlink expansion could weaken.
  3. LLM self-corrects — before modifying any file, the LLM will call ReadFile/Glob first, at which point it sees the content is unrelated to the project and moves on.

The P2 (sort before truncation) has already been fixed in the merged code.

Comment on lines +74 to +76
total += 1
if len(entries) >= max_width:
continue
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 Sort entries before applying width truncation

_collect_entries enforces max_width while iterating and only sorts afterward, so the returned subset depends on raw iterdir() order instead of the documented dirs-first/alphabetical order. In directories with more than the cap, entries that should sort earlier (especially directories) can be dropped entirely if they appear later in filesystem iteration, making the prompt context unstable and misleading. Apply truncation after ordering (or maintain a sorted top-k) to keep selection deterministic.

Useful? React with 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

Add [drwxr-xr-x] / [-rw-r--r--] / [?] permission prefixes to each
entry so the LLM can distinguish executable scripts, read-only files,
and stat-failed entries. Extract _format_mode() helper using bit-shift
loop for cleaner permission string generation.
Copy link
Copy Markdown

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

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: a85e1cb867

ℹ️ 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 on lines +106 to +109
├── adir/
│ └── inside.txt
├── emptydir/
├── regular.txt\
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 Restore mode prefixes in Windows tree snapshot

The updated Windows snapshot omits the [mode] prefix on every line, but list_directory() now always emits each entry as [{mode}] name (including on Windows via src/kimi_cli/utils/path.py in the root and child formatting paths). As written, this test will fail whenever the Windows job runs, so the PR can pass on Linux/macOS while still breaking cross-platform CI coverage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +104 to +111
assert out == snapshot(
"""\
├── adir/
│ └── inside.txt
├── emptydir/
├── regular.txt\
"""
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Windows test snapshot missing [mode] formatting and has wrong last-entry connector

The test_list_directory_tree_windows expected snapshot is incorrect. list_directory unconditionally formats every entry with [{mode}] (see src/kimi_cli/utils/path.py:125, :139, :143), and uses └── for the last entry when remaining == 0 (src/kimi_cli/utils/path.py:121-122). The Windows test expects entries without [mode] and uses ├── for the final entry regular.txt instead of └──.

Expected vs actual output on Windows

Expected in test:

├── adir/
│   └── inside.txt
├── emptydir/
├── regular.txt

Actual code would produce:

├── [drwxrwxrwx] adir/
│   └── [-rw-rw-rw-] inside.txt
├── [drwxrwxrwx] emptydir/
└── [-rw-rw-rw-] regular.txt

This test will always fail when run on a Windows platform.

Suggested change
assert out == snapshot(
"""\
├── adir/
│ └── inside.txt
├── emptydir/
├── regular.txt\
"""
)
assert out == snapshot(
"""\
├── [drwxrwxrwx] adir/
│ └── [-rw-rw-rw-] inside.txt
├── [drwxrwxrwx] emptydir/
└── [-rw-rw-rw-] regular.txt\
"""
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved: The [mode] formatting was removed entirely in a subsequent commit ('fix: remove permission display from tree listing'). The current code no longer includes permission strings, and the Windows test snapshot at lines 143-149 now correctly shows entries without [mode] and uses └── for the last entry. Bug is resolved.

@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Apr 10, 2026

对比如下:

旧版(flat ls,无深度,无上限)

drwxr-xr-x        128 .context
-rw-r--r--       2870 flake.lock
drwxr-xr-x        544 tests_e2e
-rw-r--r--         29 pytest.ini
...
drwxr-xr-x         96 src
  • 36 行 / 1133 字符 / ~283 tokens(本项目)
  • 只有一层,无排序(目录文件混在一起)
  • 看不到 src/ 里有什么、tests/ 分了哪些模块
  • 遇到 5000+ 文件的目录会直接炸 token limit(280K tokens)

新版(2 层树 + 深度/宽度限制)

├── .github/
│   ├── ISSUE_TEMPLATE/
│   ├── workflows/
│   ├── dependabot.yml
│   └── pull_request_template.md
├── scripts/
│   ├── build_vis.py
│   └── install.sh
├── tests/
│   ├── auth/
│   ├── tools/
│   ├── conftest.py
│   └── ... and 10 more             ← 截断提示
├── pyproject.toml
└── ... and 6 more entries           ← 根级截断
  • 126 行 / 2520 字符 / ~630 tokens(本项目)
  • 目录优先排序,字母序
  • 展开第二层:一眼看到 tests/ 下有 auth/, tools/, conftest.py
  • 双重截断保护:根级 30 条、子级 10 条,... and N more 提示剩余数量
  • 系统提示词告诉 AI 树是截断的,可用 Glob/Shell 探索更多
  • 5000 文件目录:最多输出 30 + 30×10 = 330 行 ≈ 2.5K tokens(vs 旧版 280K tokens 炸掉)

关键差异总结

旧版 新版
深度 1 层 2 层
排序 目录优先 + 字母序
宽度限制 无(全列) 根 30 / 子 10
截断提示 ... and N more
权限/大小 无(/ 后缀区分目录)
5000 文件目录 280K tokens 炸掉 ~2.5K tokens
系统提示词 无说明 告诉 AI 树是截断的,用 Glob 探索

The comparison is as follows:

Old version (flat ls, no depth, no upper limit)

drwxr-xr-x 128 .context
-rw-r--r-- 2870 flake.lock
drwxr-xr-x 544 tests_e2e
-rw-r--r-- 29 pytest.ini
...
drwxr-xr-x 96 src
  • 36 lines / 1133 characters / ~283 tokens (this project)
  • Only one layer, no sorting (directory files mixed together)
  • Can't see what's in src/ and which modules are divided into tests/
  • When encountering a directory with 5000+ files, the token limit will be exploded (280K tokens)

New version (2-level tree + depth/width restrictions)

├── .github/
│ ├── ISSUE_TEMPLATE/
│ ├── workflows/
│ ├── dependabot.yml
│ └── pull_request_template.md
├── scripts/
│ ├── build_vis.py
│ └── install.sh
├── tests/
│ ├── auth/
│ ├── tools/
│ ├── conftest.py
│ └── ... and 10 more ← Truncate prompt
├── pyproject.toml
└── ... and 6 more entries ← Root level truncation
  • 126 lines / 2520 characters / ~630 tokens (this project)
  • Directory priority sorting, alphabetical order
  • Expand the second layer: At a glance, you can see auth/, tools/, conftest.py, etc. under tests/
  • Double truncation protection: 30 items at the root level, 10 items at the sub-level, ... and N more prompts the remaining quantity
  • The system prompt word tells the AI that the tree is truncated, you can use Glob/Shell to explore more
  • 5000 file directory: maximum output 30 + 30×10 = 330 lines ≈ 2.5K tokens (vs the old version 280K tokens exploded)

Summary of key differences

Old version New version
Depth 1 level 2 levels
Sort None Directory first + alphabetical order
width limit none (full column) root 30 / sub 10
Truncation hint None ... and N more
Permissions/Size Yes No (/ suffix distinguishes directories)
5000 file directory 280K tokens blown up ~2.5K tokens
System prompt word No description Tell AI that the tree is truncated and use Glob to explore

Drop [drwxr-xr-x] prefixes — directory entries already end with /
which is sufficient for the LLM to distinguish files from directories.
Copy link
Copy Markdown

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

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: 3b1a5b4d13

ℹ️ 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 on lines +75 to +76
if len(entries) >= max_width:
continue
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 Stop scanning full directories after width cap is hit

_collect_entries keeps iterating through every remaining entry even after max_width is reached, and list_directory() invokes it for each expanded top-level directory. In workspaces where early directories are very large (for example node_modules/vendored trees), prompt generation still does full O(n) directory scans and can significantly delay startup despite truncating displayed output; this is a performance regression from the previous top-level-only listing. Consider short-circuiting once overflow is detected (e.g., max_width + 1) instead of computing an exact remainder count.

Useful? React with 👍 / 👎.

n-WN added 2 commits April 10, 2026 17:58
… tests

- Return "(empty directory)" instead of "" for empty directories
- Add token budget comment to _LIST_DIR_ROOT_WIDTH constant
- Fix test_truncates_child_width: assert exact prefix instead of .strip()
- Add tests: empty dir, unreadable subdir, last-entry-is-dir prefix
_collect_entries now stats and sorts ALL entries before slicing to
max_width, ensuring the displayed subset is deterministic regardless
of filesystem enumeration order. Previously the first max_width entries
by iterdir order were taken then sorted, which could drop directories
that happen to appear late in enumeration.

Also fix Windows test snapshot: regular.txt is last entry → └── not ├──.
@RealKai42 RealKai42 merged commit c6dd099 into main Apr 10, 2026
15 checks passed
@RealKai42 RealKai42 deleted the n-WN/repro-token-limit-1809 branch April 10, 2026 12:43
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.

空上下文提示token limit || Empty context prompt token limit

2 participants