Skip to content

fix(file): preserve CRLF during file edits#1953

Open
Kingwl wants to merge 3 commits intoMoonshotAI:mainfrom
Kingwl:codex/preserve-crlf-file-edits
Open

fix(file): preserve CRLF during file edits#1953
Kingwl wants to merge 3 commits intoMoonshotAI:mainfrom
Kingwl:codex/preserve-crlf-file-edits

Conversation

@Kingwl
Copy link
Copy Markdown

@Kingwl Kingwl commented Apr 20, 2026

Related Issue

Fixed #1952

Description

Summary

Fix CRLF line-ending churn in file tools.

ReadFile and StrReplaceFile now preserve original line endings when reading files, so editing a CRLF file no longer rewrites the whole file as LF. KAOS text read APIs now accept a newline parameter and pass it through local/SSH/ACP backends.

Changes

  • Add newline support to KAOS readtext / readlines APIs.
  • Preserve original line endings in ReadFile and StrReplaceFile.
  • Add CRLF coverage for KAOS, ReadFile, and StrReplaceFile.
  • Update changelog and docs.

Testing

  • make gen-changelog
  • make gen-docs
  • uv run pytest packages/kaos/tests/test_local_kaos.py tests/tools/test_read_file.py tests/tools/test_str_replace_file.py
  • Pre-commit hooks ran on commit:
    • make format-pykaos
    • make check-pykaos
    • make format-kimi-cli
    • make check-kimi-cli

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open in Devin Review

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

ℹ️ 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".


# Read the file content
content = await p.read_text(errors="replace")
content = await p.read_text(errors="replace", newline="")
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 LF fallback when matching edits on CRLF files

Reading with newline="" keeps \r\n in content, but replacement still uses exact string matching. On CRLF files, an edit payload that uses LF ("...\n") no longer matches and returns “No replacements were made,” which regresses common agent/client behavior where generated text is LF-normalized. This means edits can fail even when the visible text matches; consider a compatibility fallback that normalizes line endings for matching while preserving original endings on write.

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 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +238 to 241
newline: Newline = None,
) -> str:
async with self._sftp.open(str(path), "r", encoding=encoding, errors=errors) as f:
return await f.read()
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.

🔴 SSHKaos.readtext silently ignores newline parameter, breaking CRLF preservation over SSH

The newline parameter was added to SSHKaos.readtext to match the Kaos protocol (packages/kaos/src/kaos/__init__.py:184), but it is never forwarded to self._sftp.open(). The SFTP file is always opened with default (universal) newline handling, which normalizes \r\n to \n. When tools like ReadFile and StrReplaceFile pass newline="" to preserve CRLF line endings (as done in src/kimi_cli/tools/file/read.py:181 and src/kimi_cli/tools/file/replace.py:132), the request is silently ignored on the SSH backend. This means the entire CRLF-preservation fix introduced by this PR does not work over SSH — CRLF files will still have their line endings silently converted to LF when read, and StrReplaceFile edits will fail to match CRLF content.

Prompt for agents
The SSHKaos.readtext method accepts a newline parameter but never passes it to self._sftp.open(). This means CRLF line endings are always normalized to LF during reads over SSH, defeating the purpose of this PR.

The fix depends on whether asyncssh's SFTPClient.open() supports a newline parameter. If it does, simply pass newline=newline to the open() call at line 240. If it does not, you'll need to read the file as binary (mode='rb'), decode the bytes manually with the given encoding/errors, and return the result without newline translation. The same approach should be mirrored in readlines().

Files to check:
- packages/kaos/src/kaos/ssh.py: SSHKaos.readtext (line 232-241) and SSHKaos.readlines (line 243-254)
- asyncssh SFTPClient.open() API documentation for newline parameter support
Open in Devin Review

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

Comment thread src/kimi_cli/acp/kaos.py
Comment on lines 240 to 241
response = await self._client.read_text_file(path=abs_path, session_id=self._session_id)
return response.content
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.

🔴 ACPKaos.readtext silently ignores newline when ACP client handles reads

When self._supports_read is True, ACPKaos.readtext calls self._client.read_text_file() which does not accept a newline parameter — the newline argument is silently discarded. According to src/kimi_cli/acp/AGENTS.md:84, ReadFile uses KaosPath.read_lines which ACPKaos implements via ACP reads. So when an ACP session reads a CRLF file with newline="" (as ReadFile and StrReplaceFile now do), the line-ending preservation requested by the caller is ignored if the ACP client normalizes line endings. This breaks the CRLF-preservation fix for IDE integrations that use ACP (e.g. Zed).

Prompt for agents
When self._supports_read is True, ACPKaos.readtext delegates to self._client.read_text_file() which has no newline parameter. The newline argument passed by callers like ReadFile (newline='') is silently ignored.

Possible approaches:
1. When newline is not None (i.e. caller requests non-default newline handling), fall back to self._fallback.readtext() which reads from the local filesystem and respects the newline parameter. This would work when the ACP client and server share a filesystem.
2. If ACP clients may be on a different machine, consider reading via readbytes() and decoding manually when newline='' is requested.
3. Extend the ACP read_text_file protocol to support a newline parameter.

File: src/kimi_cli/acp/kaos.py, method ACPKaos.readtext (line 227-241)
Open in Devin Review

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

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

ℹ️ 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 on lines +238 to 240
newline: Newline = None,
) -> str:
async with self._sftp.open(str(path), "r", encoding=encoding, errors=errors) as f:
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 Forward newline to SSH text reads

SSHKaos.readtext() now accepts a newline argument, but the value is never passed into _sftp.open(). Callers introduced in this commit (ReadFile/StrReplaceFile) now rely on newline="" to preserve original line endings, so SSH-backed sessions still use backend defaults instead of the requested behavior and can continue normalizing CRLF content during read/edit flows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Nope. Model response should keep line feed

Comment thread src/kimi_cli/acp/kaos.py
Comment on lines 236 to 240
if not self._supports_read:
return await self._fallback.readtext(abs_path, encoding=encoding, errors=errors)
return await self._fallback.readtext(
abs_path, encoding=encoding, errors=errors, newline=newline
)
response = await self._client.read_text_file(path=abs_path, session_id=self._session_id)
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 Respect newline option for ACP read_text_file path

In ACPKaos.readtext(), the new newline parameter is only honored in the local fallback branch; when ACP fs.readTextFile is available, the method returns response.content directly and ignores newline. This makes the CRLF-preservation behavior backend-dependent, so file-edit/read calls that pass newline="" can still lose expected line-ending semantics depending on the ACP client implementation.

Useful? React with 👍 / 👎.

…ile-edits

# Conflicts:
#	CHANGELOG.md
#	docs/en/release-notes/changelog.md
#	docs/zh/release-notes/changelog.md
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.

CRLF issue with StrReplaceFile

1 participant