Skip to content

fix(agent-core-v2): preserve BOM and encoding when Edit modifies files - #4371

Open
NoX-programer wants to merge 20 commits into
MoonshotAI:mainfrom
NoX-programer:fix/edit-preserve-unicode-bom
Open

NoX-programer wants to merge 20 commits into
MoonshotAI:mainfrom
NoX-programer:fix/edit-preserve-unicode-bom

Conversation

@NoX-programer

Copy link
Copy Markdown

Requirement or Bug

Resolve #3825

Bug Reproduction Steps

See linked issue.

Root Cause

The Edit tool's read path decodes file content with a UTF-8-only TextDecoder (default ignoreBOM: false), which silently strips a leading byte order mark (BOM), and the write path always emits UTF-8. So every time Edit modified a file that started with a BOM, the BOM and the original encoding were lost: the file came back as plain UTF-8 without a BOM.

This is a fundamental fix: the read and write paths now detect the BOM and round-trip the original encoding.

Code Changes

  • packages/agent-core-v2/src/_base/text/encoding.ts — new BOM helpers: splitByteOrderMark / byteOrderMarkBytes (all five BOMs: UTF-8, UTF-16LE/BE, UTF-32LE/BE) and strict decodeUnicodeText / encodeUnicodeText for those encodings.
  • packages/agent-core-v2/src/app/edit/fileEditService.ts — dual-path edit: files without a BOM go through the existing code path unchanged; files with a BOM are read via the text API (so unsaved client buffers in ACP mode are respected), with a strict byte-level fallback if the text read cannot represent the content, and written back as BOM + original encoding.
  • packages/acp-server/src/acp-fs/acpFsService.ts — the writeBytes bridge now decodes BOM-prefixed UTF-8/UTF-16/UTF-32 payloads into text so the ACP client's buffer is updated and the client saves the file in its own encoding; binary payloads still go to the local backend.
  • packages/agent-core-v2/src/agent/tools/os/read/readTool.ts — updated the model-visible hint for transcoded files: Edit preserves BOM-prefixed encodings; Write still expects UTF-8.
  • Tests: new BOM coverage in encoding.test.ts, edit.test.ts, and acp-fs.test.ts.

Related: #3946 (skywolf) fixes the same issue for the UTF-8 BOM plus line endings and .editorconfig; this PR keeps the change surface small (Edit only, no line-ending or .editorconfig changes) and generalizes the BOM handling to all five BOM-prefixed Unicode encodings (UTF-8/UTF-16LE/BE/UTF-32LE/BE).

Behavior Changes and Affected Users

Behavior Before After Who relies on the old behavior Escape hatch
Edit on a file with a UTF-8/UTF-16/UTF-32 BOM BOM and encoding silently dropped; file rewritten as plain UTF-8 BOM and original encoding preserved None — the old behavior was data loss None needed
Edit on a file without a BOM Read/write via text API, UTF-8 Unchanged (same code path) All existing users N/A
Edit on a BOM file whose text read cannot represent the bytes N/A (old code mis-decoded) Strict byte-level fallback re-decodes the raw bytes None N/A
ACP writeBytes with a BOM-prefixed text payload Written to the local backend, client buffer not updated Bridged to writeTextFile so the client buffer is updated None N/A
Read tool hint for transcoded files "Edit and Write expect UTF-8 — convert the file's encoding first (e.g. iconv via Bash)." "Edit preserves BOM-prefixed encodings; Write expects UTF-8 — convert the file's encoding first (e.g. iconv) otherwise." Models parsing the hint N/A

Affected modules and test coverage: agent-core-v2 _base/text (encoding helpers — encoding.test.ts, round-trips for all five BOMs), agent-core-v2 app/edit (dual-path edit — edit.test.ts, BOM + encoding preserved for all five BOMs, no BOM added to BOM-less files), acp-server acp-fs (writeBytes bridge — acp-fs.test.ts, bridge for all five BOMs).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • The behavior-change table above is complete, and every removed behavior or flipped default is named in the changeset and either has an escape hatch or was explicitly approved by a maintainer in this PR.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Note on the second checkbox: #3825 is linked above; per repo policy, a maintainer's /approve on the issue is still pending.

Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d319e30

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T10:23:01.066566Z d319e30 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 8e209735fb

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

async edit(input: FileEditInput, fs: IHostFileSystem = this.fs): Promise<FileEditResult> {
try {
const raw = await fs.readText(input.path, { errors: 'strict' });
const bom = splitByteOrderMark(await fs.readBytes(input.path, BOM_SNIFF_BYTES)).bom;

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 Fall back to ACP text reads when byte sniffing is unavailable

When Edit runs through AcpHostFileSystem for a client-backed buffer whose path is absent or inaccessible on the agent's local disk, this unconditional readBytes fails before readText can use the ACP reverse-RPC, because AcpHostFileSystem.readBytes always delegates to its local HostFileSystem. The previous implementation began with readText, so the same client buffer was editable; treat an unavailable BOM sniff as the BOM-less text path (or add a client byte capability) instead of making local bytes a prerequisite.

AGENTS.md reference: AGENTS.md:L122-L124

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.

Fixed: the BOM sniff is now wrapped in try/catch — when readBytes is unavailable (e.g. an ACP client-backed buffer, where AcpHostFileSystem.readBytes delegates to the local disk backend and throws), Edit falls back to the BOM-less text path via readText, which is the pre-PR behavior. Added a regression test simulating a rejecting readBytes.

try {
const text = await fs.readText(path, options);
if (!text.includes('\0')) {
return stripLeadingBomChar(text);

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 a leading U+FEFF content character

When an ACP client decodes a BOM-prefixed file whose first content character is itself U+FEFF, readTextFile returns that content character after consuming the encoding marker, but this branch strips it unconditionally; the previous Edit path forwarded it to writeTextFile. The fallback decodeUnicodeText repeats the loss for UTF-8/UTF-16 bodies because its decoder also treats the already-split character as a BOM, so a successful edit silently deletes file content; only strip the actual byte marker and decode the body with ignoreBOM: true.

AGENTS.md reference: AGENTS.md:L130-L132

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.

Fixed: the unconditional U+FEFF strip is gone — the byte marker is already consumed by the readText decoders (and by ACP clients), so a leading U+FEFF in the returned text is content, not a marker. decodeUnicodeText now decodes the BOM-split body with ignoreBOM: true, so the byte path keeps it as well. Added regression tests for both paths (UTF-8 and UTF-16LE files whose content starts with U+FEFF, plus direct decodeUnicodeText cases for all encodings).

Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>

@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: d3b8dc7bb1

ℹ️ 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 +27 to +30
if (!text.includes('\0')) {
return text;
}
} catch {}

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 Keep BOM edits on the ACP client buffer

When a BOM-prefixed file is edited through AcpHostFileSystem and the node-local copy is readable, either a legitimate U+0000 in the client result or any readTextFile failure enters this fallback. AcpHostFileSystem.readBytes in packages/acp-server/src/acp-fs/acpFsService.ts:139-140 reads that local copy, but the later writeBytes is bridged back to writeTextFile; if the editor buffer has unsaved changes, the edit is computed from stale disk data and silently overwrites the buffer, whereas the previous path used the client text or surfaced its error. Restrict this fallback to known local decoder failures instead of rejecting NUL/all ACP errors.

AGENTS.md reference: AGENTS.md:L130-L132

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.

Fixed: IHostFileSystem now carries an optional clientBackedText marker, set by AcpHostFileSystem when the fs.readTextFile capability is negotiated. readBomText uses only the client text for such filesystems — no byte reads, so stale local data can never reach the write path, and readTextFile errors surface instead of being swallowed. Local filesystems read the authoritative bytes directly, which also removes the NUL mis-decode heuristic. Added regression tests for the marker and for the client-backed edit path.

Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
@NoX-programer

Copy link
Copy Markdown
Author

@codex review

@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: 4b66c79e93

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

const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
const little = encoding === 'utf-32le';
for (let i = 0; i < units.length; i++) {
view.setUint32(i * 4, units[i]!.codePointAt(0)!, little);

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 Reject unpaired surrogates before encoding

When an Edit result contains an unpaired UTF-16 surrogate, such as a lone high surrogate supplied in new_string, this writes 0xD800 as a UTF-32 code point (and the UTF-16 branch likewise emits the raw unpaired code unit), even though decodeUnicodeText rejects those values. A local Edit therefore reports success but creates a file that the next Edit cannot decode; with AcpHostFileSystem, decodeTextPayload rejects the payload and writeBytes silently updates only the node-local copy instead of the client buffer. Reject or normalize non-well-formed strings before writing them.

AGENTS.md reference: AGENTS.md:L130-L132

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.

Fixed: encodeUnicodeText now rejects strings containing unpaired surrogates (lone high or low) for all five encodings, mirroring decodeUnicodeText, which already rejects them. The Edit fails with a clear error instead of writing a file the next Edit cannot decode, and the ACP bridge no longer falls back to the node-local copy for such payloads. Added regression tests: direct encodeUnicodeText cases (unpaired high/low rejected, paired astral characters accepted, all five encodings) and an end-to-end Edit test asserting the write is never attempted.

Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
Signed-off-by: NoXъ <NoX-programer@users.noreply.github.com>
@NoX-programer

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: d319e303b6

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

This branch has not been deployed

No deployments
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.

Kimi code内置Edit工具剥掉BOM问题

1 participant