Skip to content

📍 feat: Show the Current Text Where a Missing Workspace Edit Belongs - #16698

Merged
danny-avila merged 5 commits into
devfrom
danny-avila/edit-conflict-hints
Oct 3, 2026
Merged

danny-avila merged 5 commits into
devfrom
danny-avila/edit-conflict-hints

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

When an edit_file in an attached (BYOM) workspace misses, LibreChat tells the model which edit failed and a line number at best. #16520 deliberately dropped the one file excerpt workers sent, and nothing else let the model see what the file actually says there. So the usual next step is a read_file of the whole file, then another attempt. LibreChat-AI/code-interpreter#300 makes the worker quote the current text of the region the edit most likely meant, marked line by line. This PR renders that excerpt for the model, keeps it out of the logs, and fixes a mislabeled 409.

Depends on LibreChat-AI/code-interpreter#300 for the excerpts. Merging this first is safe: without that worker change, the messages are unchanged.

Evidence (demo.librechat.ai, last 7 days)

  • 2,643 attached edit_file calls; 90 failed with EDIT_CONFLICT (3.4%).
    • 79 came from a worker build without per-edit diagnostics (before code-interpreter#271).
    • 2 were rendered as generic retry guidance. One was a legacy worker body; the other was a WORKSPACE_QUARANTINED 409 that LibreChat called a text mismatch.
  • Both demo workers now advertise tolerant_match, and LibreChat sends matching: 'tolerant' by default (🎯 feat: Tolerant Workspace Edits with Host-Rendered Conflict Diagnostics #16520). Whitespace drift therefore no longer fails, and ambiguous matches already list every location.
  • Comparing each failed old_text with the next successful edit to the same file:
    • most common: the retry added context (an ambiguous match);
    • next: 1–3 lines differed (stale or misremembered content);
    • then: whitespace or indentation only;
    • none involved line-number prefixes, elisions or CRLF.
  • 35 of 90 failures were followed by a read_file of the same file before the retry. The stale-content case is the one this PR targets.

How it works

Excerpt parsing (code/edits.ts). The worker's excerpt is always its last hint:

the current text at lines A-B (~ whitespace differs, ! text differs) is "<line>|<mark><text>\n..."

parseEditConflict keeps it only if every row is well formed:

  • line numbers are contiguous and match the stated range, with at most 8 rows;
  • each mark is a space, ~ or !;
  • each line is at most 161 characters (the worker's 160 plus an ellipsis), with no control characters other than tab and no U+2028/U+2029.

Otherwise the excerpt is dropped and the other hints are kept. A complete JSON-quoted closest-line snippet is now skipped, so an excerpt after it can be read. The snippet text itself is still never kept.

Rendering (formatEditConflict(path, report, withExcerpts)). With excerpts, each missing edit says which lines differ, then shows a block numbered like read_file output:

The edit to "workspace/src/app.ts" did not apply, so nothing was written: old_text was not found; its first line is at line 1, but the lines after it differ; the closest match is at lines 1-3, where line 2 differs; correct old_text against the current text below (it may leave out lines or shorten them with "…"; read_file shows them in full).
Current text (! text differs, ~ only whitespace differs):
  1 | function load(user) {
! 2 |   return fetchUser(user.id);
  3 | }

The block is presented as partial context: the worker shows at most 8 rows and shortens wide lines, so the model is pointed to read_file for anything left out. In a batch, each block follows its own Edit N: line. Without excerpts the output is exactly what #16520 produced.

Trust and logging (agents/handlers.ts). #16520 forwards no free text from a worker, and that does not change: every sentence is still LibreChat's own, built from parsed facts. The excerpt is the exception, because it is file content from the file being edited, which the same worker already returns through read_file. It is handled like a read_file result:

  • It is quoted only when the selected workspace allows read_file, so a failing edit can never reveal file text that the operation policy withholds. The worker applies the same rule on its side.
  • It must pass filteredFileResult, the file-content policy. If the policy would block it, the model gets the excerpt-free message.
  • It is part of the model-facing errorMessage, so the tool-output filter still sees it.
  • It never reaches logs. A new AttachedEditRejectionError carries the log-safe text in message and the excerpt version in modelMessage. getSafeToolError gives the model modelMessage and keeps errorMessage in the log context excerpt-free. The logged upstreamBody stays the sanitized {"code":"EDIT_CONFLICT"}.

Other 409s. A Code API rejection such as WORKSPACE_QUARANTINED used to be rendered as "the requested text did not match exactly once; re-read the file and retry". Re-reading cannot fix a quarantined workspace. WorkspaceToolHttpError now exposes upstreamCode from a complete JSON body, restricted to upper-case code names. Only EDIT_CONFLICT, or a body without a readable code, goes through the conflict path. Any other 409 gets a host-worded message that names only its code, plus recovery guidance for WORKSPACE_QUARANTINED ("must be reset on its machine before edits can apply, so retrying will not help"). Its body is never forwarded or logged.

Also:

  • The worker's repetitive-candidate refusal is recognized instead of falling back to generic guidance.
  • The attached edit_file descriptions mention the quoted text.

Compatibility. Excerpts appear only with workers that send them (code-interpreter#300). Older workers produce the same messages as before. Older LibreChat builds drop the excerpt hint and render what they render today.

Type of change

  • Feature
  • Bug fix

Testing

Tested environments/configuration:

  • Node 24.16.0; unit tests. The live path needs code-interpreter#300 on a worker.

Automated tests:

  • packages/api: focused code and agent-handler suites, run in-band with sandbox-safe MongoDB sockets: 973 passed, 1 skipped. New coverage:
    • parsing and rendering excerpts, with and without withExcerpts;
    • an excerpt after a complete closest-line quote, and blocks placed under their own edits in a batch;
    • dropping malformed excerpts while keeping the other hints: out-of-range or missing rows, too many rows, unknown marks, overlong lines, control characters, line separators;
    • tabs kept in quoted lines;
    • quoted text confined to the numbered block;
    • bounded output for a batch of maximal excerpts;
    • the repetitive-candidate reason;
    • upstreamCode parsing (complete bodies only, code names only);
    • handler level: the model sees the excerpt while the logged errorMessage and upstreamBody contain no file text; a file-content policy match, or a workspace without read_file, drops the excerpt; a WORKSPACE_QUARANTINED 409 is named by its code, with neither the model message nor the logs carrying its body.
    • streamed Unicode rejection bodies at 3,800, 4,096, and 4,097 bytes, including split UTF-8 sequences;
    • quoted file text remains subject to tool-output filtering and absent from logs.
  • npx tsc --noEmit in packages/api; npm run build:api; npm run static-checks -- --against origin/dev (ESLint, Prettier, import sorting, manifest validation, circular dependencies).

Screenshots / recordings

Not applicable. No UI change; only the text of a failed edit's tool result changes.

Render the current-text excerpt that LibreChat-AI/code-interpreter#300 workers append to a missing edit's EDIT_CONFLICT diagnosis: a strict parse keeps it only when every row is well formed (contiguous numbers matching the stated range, a known mark, a bounded single line), and LibreChat renders it as a numbered block under its edit with a summary of which lines differ. The excerpt passes the file-content policy like a read_file result and reaches only the model; logs keep the excerpt-free message.

Also stop rendering every 409 as a text mismatch: a Code API rejection such as WORKSPACE_QUARANTINED now passes through with its own reason, and the worker's repetitive-candidate refusal is recognized.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 01:53
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 1 of 4 on head 862281d.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 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-03T03:01:55.033173Z 11a8aa8 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.

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Valid Unicode excerpts exceed the transport byte limit and lose their diagnostics before parsing.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Improves attached-workspace edit failures by showing validated current-text excerpts to the model while keeping file content out of logs.

Changes:

  • Parses and renders bounded excerpts and repetitive-match diagnostics.
  • Applies file-content filtering and separates model-facing errors from logs.
  • Distinguishes edit conflicts from other HTTP 409 rejections.
File Description
packages/​api/​src/​code/​workspace.ts Extracts codes from complete error bodies.
packages/​api/​src/​code/​workspace.spec.ts Tests error-code parsing.
packages/​api/​src/​code/​edits.ts Parses, validates, and renders excerpts.
packages/​api/​src/​code/​edits.spec.ts Tests excerpt handling and diagnostics.
packages/​api/​src/​agents/​tools.ts Documents excerpt-based retry guidance.
packages/​api/​src/​agents/​handlers.ts Filters excerpts and separates model and log messages.
packages/​api/​src/​agents/​handlers.spec.ts Tests filtering, logging, and rejection handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

(upstreamBodyTruncated ? ' [body truncated or incomplete]' : ''),
);
this.name = 'WorkspaceToolHttpError';
const parsed = upstreamBodyTruncated ? undefined : parseErrorBody(upstreamBody);

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.

Addressed at the producer rather than by raising the reader's cap. LibreChat-AI/code-interpreter#300 now measures excerpts and the whole diagnostic as UTF-8 bytes once JSON-encoded, the same measure as the Code API's {error, code} body:

  • each excerpt is capped at 1,600 bytes, dropping trailing rows to fit;
  • the message is capped at 3,800 bytes, with excerpts granted only within that and reasons shortened by both length and bytes.

The worker's tests include your example: eight rows of wide CJK lines yield fewer rows, and the full error body stays within 4,096 bytes. It also covers escape-heavy batches and emoji-heavy closest-line quotes.

I kept MAX_ERROR_BODY_BYTES at 4,096. It bounds every upstream error body, not only edit conflicts, and the producer now guarantees the diagnostic fits it. The complete-body guard stays as the safety net for anything that does not.

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.

Added HTTP-stream regression coverage in c95247b. Unicode diagnostics survive complete 3,800- and 4,096-byte bodies, including split UTF-8 sequences; a 4,097-byte body drops both code and diagnostic. The producer byte budget and 4,096-byte transport cap remain unchanged.

Comment thread packages/api/src/code/edits.ts Outdated
describeExcerptLines(excerptLinesWith(excerpt, 'changed'), ''),
describeExcerptLines(excerptLinesWith(excerpt, 'whitespace'), ' only in whitespace'),
].filter((part): part is string => part != null);
return `the closest match is at ${where}${differences.length > 0 ? `, where ${differences.join(' and ')}` : ''}; copy old_text from the current text below`;

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.

Agreed, fixed in 6c643b5. The summary no longer says to copy old_text from the block. It now reads "correct old_text against the current text below (it may leave out lines or shorten them with "…"; read_file shows them in full)". The attached edit_file description says the same: fix the edits against any quoted current text, and use read_file for lines it leaves out or shortens. A new formatter test renders a windowed 8-row excerpt with a shortened line, and checks the guidance and the verbatim shortened row.

@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: 862281d71d

ℹ️ 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 packages/api/src/agents/handlers.ts Outdated
Comment on lines +4129 to +4132
/** Other 409s (a quarantined workspace) are the Code API's own rejections, not edit misses. */
const isEditConflict = error.upstreamCode == null || error.upstreamCode === 'EDIT_CONFLICT';
if (error.upstreamStatus === 409 && isEditConflict) {
throw attachedEditConflict(tc, req, path.filePath, error);

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 Sanitize non-edit 409 responses before surfacing them

When an attached worker returns a 409 with any recognized code other than EDIT_CONFLICT (for example, WORKSPACE_QUARANTINED), this condition bypasses attachedEditConflict and rethrows the original WorkspaceToolHttpError. The outer tool handler then exposes and logs its message, which embeds the worker-controlled upstreamBody; therefore arbitrary diagnostic text, submitted content, or prompt-injection text can reach the model and logs. Preserve the code/status but translate these rejections to an approved safe message and sanitized log metadata rather than forwarding the body.

AGENTS.md reference: AGENTS.md:L122-L128

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.

Fixed in f292d03. A 409 whose code is not EDIT_CONFLICT no longer rethrows the original error. It becomes a host-worded message that names only the validated code (upper-case letters, digits and _, at most 64 characters), with recovery guidance for WORKSPACE_QUARANTINED. Its logged body is just {"code":"WORKSPACE_QUARANTINED"}. The new test sends an injected quarantine body and asserts that neither the model message nor the logs contain it.

For context: the Code API maps only EDIT_CONFLICT worker settlements to 409, so these bodies come from its own store errors rather than from workers. Not forwarding them is still the safer default.

Comment on lines +72 to +74
const MAX_EXCERPT_LINES = 8;
/** The worker shortens each quoted line to 160 characters plus an ellipsis. */
const MAX_EXCERPT_LINE_CHARS = 161;

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 Keep valid excerpts within the error-body transport limit

The new grammar accepts up to eight 161-character lines per excerpt, but readErrorBody in workspace.ts caps the entire upstream error response at 4096 bytes and marks an over-limit response truncated, after which WorkspaceToolHttpError deliberately does not populate editConflict. A single maximal excerpt containing multibyte source text can exceed that cap, and three maximal ASCII excerpts already exceed it, so valid worker diagnostics supported by this parser are discarded on the real HTTP path and the model receives only generic retry guidance. Bound excerpts using the transport's UTF-8 byte budget or raise the error-body limit consistently.

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.

Fixed at the producer in LibreChat-AI/code-interpreter#300 (b1a197e), which owns the message budget. Sizes are now measured as UTF-8 bytes of the JSON-encoded string, the same measure the Code API's {error, code} body uses:

  • each excerpt is capped at 1,600 bytes, dropping trailing rows to fit;
  • a batch grants excerpts only while the whole message stays within 3,800 bytes;
  • a single edit drops its excerpt rather than exceed that.

This leaves room for the wrapper under MAX_ERROR_BODY_BYTES (4,096). New worker tests check that the full error body stays within 4,096 bytes for multibyte source lines and for escape-heavy batch excerpts.

The parser's per-row limits here stay as they are. They bound what LibreChat accepts, not what fits the transport, and excerpts that fit the body always satisfy them.

…eir Body

A 409 whose code is not EDIT_CONFLICT (a quarantined workspace) now gets a host-worded message naming only its validated code, with recovery guidance for WORKSPACE_QUARANTINED, and logs keep a body of just that code.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 2 of 4 on head f292d03.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: f292d034e5

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

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Backend Platform codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 3, 2026
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 3 of 3) of head f292d03. Please flag only blocking issues: correctness, security, or data loss.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: f292d034e5

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

A worker excerpt can window a long region and shorten wide lines with an ellipsis, so the guidance now asks the model to correct old_text against it and use read_file for lines it leaves out or shortens, here and in the attached edit_file descriptions.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 4 of 4) of head 6c643b5. Please flag only blocking issues: correctness, security, or data loss.

@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: 6c643b532f

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

if (!excerpts || filteredFileResult(tc, req, filePath, excerpts) != null) {
return settle(message);
}
return settle(message, formatEditConflict(path, report, 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.

P1 Badge Require read permission before returning conflict excerpts

When a selected workspace advertises edit_file without read_file—a supported configuration exercised by agents/tools.spec.ts—this still returns worker-supplied current file text after a deliberately failing edit. That turns edit_file into a read oracle and bypasses the operation check enforced by handleWorkspaceFileRead; suppress excerpts unless selectedWorkspaceId(codeExecutionContext, 'read_file') also succeeds.

AGENTS.md reference: AGENTS.md:L186-L189

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.

Fixed in 11a8aa8. Excerpts are now quoted only when selectedWorkspaceId(codeExecutionContext, 'read_file') succeeds. Otherwise the model gets the excerpt-free message, the same as when the file-content policy blocks the excerpt. This mirrors the worker, which already replaces the whole diagnostic for workspaces without read_file/preview_edit; LibreChat now enforces the same rule against its own view of the selected workspace's operations. A new handler test drops read_file from the workspace operations and asserts that no file text is returned.

A workspace that offers edit_file without read_file now never receives the worker's current-text excerpt, so a failing edit cannot read file content the operation policy withholds.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review the latest head, final review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 11a8aa80e1

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

@lia-by-librechat

lia-by-librechat Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Head: c95247b. Conflicts resolved against dev; path-rejection guidance, policy-checked excerpts, and sanitized 409s preserved. Added Unicode HTTP-stream boundary and tool-output policy regressions.

Checks: 973 focused tests passed, 1 skipped; API typecheck and worker/declaration build passed; PR-scoped static checks passed. Lighthouse passed in CI; local browser run was blocked by missing Chromium.

Final independent review of this exact head: no findings. Backend CI is green; remaining E2E and runtime smoke checks are running.

@danny-avila
danny-avila merged commit 01c85ab into dev Oct 3, 2026
37 checks passed
@danny-avila
danny-avila deleted the danny-avila/edit-conflict-hints branch October 3, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Backend Platform codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants