Skip to content

📍 feat: Quote the Current Text Where a Missing Workspace Edit Belongs - #300

Merged
danny-avila merged 4 commits into
mainfrom
danny-avila/edit-conflict-hints
Oct 3, 2026
Merged

danny-avila merged 4 commits into
mainfrom
danny-avila/edit-conflict-hints

Conversation

@danny-avila

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

Copy link
Copy Markdown
Collaborator

Summary

When an attached-workspace edit_file misses, the model gets a line number at best ("its first line appears at line 41, but the lines after it differ", "the closest line is line 3605") or nothing at all ("old_text was not found."). It then re-reads the file, often entirely, to find out what actually changed. This PR makes the worker quote the current text of the region the edit most likely meant, marked line by line, so the next attempt can copy it directly.

Evidence (last 7 days)

  • 2,643 attached edit_file calls; 90 failed with EDIT_CONFLICT. 79 came from a worker build before 🎯 feat: Diagnose Every Failing Workspace Edit and Negotiate Tolerant Matching #271, which returned only "Workspace edit must match exactly once".
  • Since workers picked up 🎯 feat: Diagnose Every Failing Workspace Edit and Negotiate Tolerant Matching #271 (both demo workers now advertise tolerant_match, and LibreChat sends matching: 'tolerant' by default), 11 conflicts remain: 6 ambiguous with line numbers, 3 bare "old_text was not found.", 1 "closest line is line N" and 1 rendered generically.
  • Comparing each failed old_text with the next successful edit to the same file: the most common fix added context (an ambiguous match). The next most common changed 1–3 lines (stale or misremembered content), then whitespace or indentation only. No failure involved line-number prefixes, elisions or CRLF. 35 of 90 failures were followed by a read_file of the same file before the retry.

Whitespace drift is now handled by tolerant matching, and ambiguous matches already report every location. What is left is the stale-content case, where a line number alone is not enough to act on.

Change

describeMissing (packages/code/src/edits.ts) now also finds the region the edit most likely meant:

  1. Alignment vote: each distinctive old_text line (at least two consecutive letters, digits or _, so } and ); never anchor) votes for the start line at which it would fall into place, compared without surrounding whitespace. The start with the most votes (ties: earliest) wins if at least two lines agree. One pass over the file; at most 200,000 votes.
  2. Otherwise, the existing first-line search, when that line is unique, or the existing closest line by shared tokens.

The reason then ends with one more hint:

Workspace edit did not apply and nothing was written: old_text was not found; its first line appears at line 1, but the lines after it differ; the current text at lines 1-4 (~ whitespace differs, ! text differs) is "1| export function load(user) {\n2|~  const id = user.id;\n3|!  return fetchUser(id);\n4| }".
  • Rows are <line>|<mark><text>. The mark compares the file line with the old_text line at the same position: a space if identical, ~ if only whitespace differs, ! if the text differs.
  • Bounds: at most 8 rows. Each line is cut at 160 characters without splitting a surrogate pair. A longer region is shown from just before its first difference.
  • Measured as the host receives it: the Code API returns the message inside a JSON {error, code} body, and LibreChat reads that body through a 4,096-byte buffer. Sizes are therefore counted as UTF-8 bytes of the JSON-encoded string. Each excerpt is capped at 1,600 bytes, dropping trailing rows to fit.
  • Message budget: the excerpt is optional, and it is the first detail dropped. A batch grants excerpts in edit order only while every position and full reason still fits both the existing 3,000-character bound and 3,800 encoded bytes. Granting one never shortens another edit's reason, and a single edit drops its excerpt before the message would exceed either bound.
  • Line separators: quoted text escapes U+2028/U+2029, which JSON.stringify leaves raw. Each batch edit therefore stays on one line for hosts that parse the message line by line.
  • Compatibility: all existing hints and their order are unchanged, and the excerpt is always the last hint. LibreChat's strict parser (LibreChat#16520) stops at the first hint it does not recognize and keeps the rest, so current hosts render exactly what they render today. A LibreChat change that renders the excerpt follows separately.
  • Exposure: unchanged. The excerpt comes from the file being edited, which the worker has just read. Workspaces without read_file/preview_edit still get the generic "source diagnostics require read access" settlement from worker.ts.

The README's EDIT_CONFLICT paragraph documents the hint grammar.

Testing

  • packages/code: npm run build; node --test dist/edits.test.js dist/workspace.test.js dist/workspace-worker.test.js dist/protocol.test.js passes (246 tests, 1 skipped). New edits.test.ts coverage:
    • the excerpt and its marks for stale lines and whitespace-only lines, in tolerant and exact mode;
    • a region found when the first line itself changed;
    • the best-aligned region winning, with ties going to the earliest;
    • a one-line closest match;
    • a long region windowed at its first difference;
    • line and excerpt bounds with emoji and control characters;
    • no excerpt when nothing resembles the edit, or for ambiguous and line-numbered edits;
    • batch excerpt granting that keeps every position and reason;
    • a single edit dropping its excerpt to stay within the bound;
    • bounded votes on a 300,000-line repetitive file, and on a 5,000-line repetitive old_text;
    • an excerpt that starts where old_text starts when it opens with blank lines;
    • escaped line separators;
    • error bodies within 4,096 bytes for multibyte lines and escape-heavy batch excerpts.

A missing edit's diagnosis now ends with a bounded excerpt of the region
old_text most likely meant: the alignment where most of its distinctive lines
fall into place (found in one pass with bounded votes), else its unique first
line or closest line. Each of at most 8 rows is `<line>|<mark><text>`, with a
space for identical lines, `~` for whitespace-only differences and `!` for
changed text; lines are shortened to 160 characters and the excerpt to 1,600.

The excerpt is a final `; the current text at lines A-B (...) is "<json>"`
hint, so hosts that parse the existing grammar drop it and keep every other
hint. Excerpts are the first detail dropped to fit the 3,000-character
message bound and never shorten another edit's reason.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 1 of 4 on head 85e78a1.

@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:00:37.149116Z c807ce4 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: 85e78a15a3

ℹ️ 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/code/src/edits.ts Outdated
Comment on lines +660 to +661
const start = index - offset;
if (start < 0) 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 Count skipped alignment candidates toward the work budget

When a large repetitive old_text contains thousands of identical anchor lines, each matching file line iterates the entire hits array, but offsets producing start < 0 bypass the budget decrement. As a result, the nominal 200,000-operation bound can still perform hundreds of millions of candidate checks; a valid 1 MiB batch with many failed edits can spend several seconds here before returning EDIT_CONFLICT. Decrement the budget for every inspected hit, including negative-start candidates, or avoid iterating offsets that cannot produce a nonnegative start.

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 104b8e3. Every inspected offset now decrements the budget, including one that would start before the file. Offsets are collected in ascending order, so the first negative start also ends that line's loop. A new test runs a 5,000-line repetitive old_text against a 20,000-line file within the time bound.

Comment thread packages/code/src/edits.ts Outdated
Comment on lines +625 to +628
const nearest = nearestLine(needle.find((line) => line.trim().length > 0), lines);
const reason = describeMissingReason(text, oldText, needle, matching, lines, nearest);
const anchor = nearest != null && (!nearest.exact || nearest.count === 1) ? nearest.index : undefined;
const start = closestRegion(needle, lines()) ?? anchor;

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 Offset fallback anchors by leading needle lines

When old_text begins with one or more blank lines and has too few distinctive lines for closestRegion, nearestLine locates the first nonblank needle line, but its file index is used directly as the region start. The excerpt is therefore shifted down by the number of leading blank lines: for example, a missing "\nconst total = items.size;" can quote the current const line as differing from the blank first needle line and then quote the unrelated following line. Preserve the nonblank line's needle offset and subtract it from nearest.index before generating the excerpt.

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 104b8e3. The fallback anchor now subtracts the first non-blank line's offset in old_text (clamped at the top of the file), so the excerpt starts where old_text starts. A new test covers "\nconst total = items.size;".

The same commit also escapes U+2028/U+2029 in quoted text. JSON.stringify leaves them raw, and a host's per-line batch parser would stop at them.

…t Starts

- Count every inspected anchor offset against the region vote budget, and stop
  at the first offset that would start before the file (offsets ascend), so a
  repetitive old_text cannot escape the bound.
- Start a first-line or closest-line excerpt at the line where old_text starts,
  not at its first non-blank line.
- Escape U+2028/U+2029 in quoted text, which JSON.stringify leaves raw, so each
  batch edit stays on one line for hosts that read the message line by line.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 2 of 4 on head 104b8e3.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 104b8e301c

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

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 3 of 3) of head 104b8e3. 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. Can't wait for the next one!

Reviewed commit: 104b8e301c

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

Code API returns the diagnostic inside a JSON error body, and LibreChat reads
that body through a 4,096-byte buffer, discarding a truncated one. A
3,000-character message with multibyte or escape-heavy excerpts could exceed
it. Excerpts are now limited to 1,600 bytes and granted only while the whole
message stays within 3,800 bytes, both measured as UTF-8 once JSON-encoded.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 4 of 4) of head b1a197e. 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: b1a197e558

ℹ️ 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/code/src/edits.ts Outdated
Comment on lines 884 to 885
const reasonLength = reasons.reduce((total, reason) => total + reason.length, 0);
const reasonLimit = reasonLength <= available ? Infinity : Math.floor(available / failures.length);

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 Enforce the encoded-byte limit when truncating batch reasons

When a batch has multiple missing edits whose nearest-line snippets contain multibyte text, excerpts are correctly declined, but the base reasons are truncated only by UTF-16 character count here. For example, 20 failures quoting emoji-heavy lines produce a 2,997-character diagnostic whose serialized {error, code} body is about 4,411 bytes, exceeding the 4,096-byte host buffer and causing the response to be discarded. Apply the encoded-byte budget while truncating reasons, or validate and shrink the final batch message.

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 c807ce4. When batch reasons still exceed the bound after excerpts are declined, each one is now shortened against both the per-reason character share and an equal share of the 3,800-byte encoded budget, with the ellipsis counted and surrogate pairs kept whole. A single edit's reason gets the same treatment instead of a bare character slice. New tests run your scenario (20 failures quoting emoji-heavy lines) and a single long multibyte reason. Both assert the serialized {error, code} body is at most 4,096 bytes, every position is present and the output is well-formed UTF-8.

Batch reasons that still exceed the bound after excerpts are declined are now
shortened against both the 3,000-character limit and the 3,800-byte encoded
budget, and a single edit's reason is shortened the same way instead of being
cut by character count. Multibyte closest-line quotes can no longer push the
Code API's error body past the 4,096 bytes hosts read.
@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. Can't wait for the next one!

Reviewed commit: c807ce4e4c

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

@danny-avila
danny-avila merged commit 893ad36 into main Oct 3, 2026
11 checks passed
danny-avila added a commit to LibreChat-AI/LibreChat that referenced this pull request Oct 3, 2026
…16698)

* 📍 feat: Show the Current Text Where a Missing Workspace Edit Belongs

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.

* 🔒 fix: Name Other Attached Edit 409s by Code Instead of Forwarding Their 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.

* 🧾 fix: Present Quoted Edit Excerpts as Partial Context, Not Text to Copy

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.

* 🔐 fix: Quote Edit Excerpts Only Where the Workspace Allows read_file

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.

---------

Co-authored-by: Lia <lia@librechat.ai>
@danny-avila
danny-avila deleted the danny-avila/edit-conflict-hints branch October 5, 2026 11:52
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.

1 participant