Conversation
One bad old_text used to discard every other file in the call. Each file now goes to the agent in its own request, so files without errors are applied and a file with an error is left unchanged. The plan-file path checks reject only their file; checks on the whole input still reject the whole call. A result with applied and failed files is a partial success, not an error. Failed files list all their edits[i] indexes because the agent does not say which edit failed. A file whose request failed without an agent response is reported as unknown, with a request to re-read it, because the write may have completed. Files not sent before the tool call was interrupted are reported as not applied, since the interrupt handler can persist this result.
…_files A file skipped because the tool call was interrupted was told to "fix and resend" its edits, though nothing was wrong with it and the user may have interrupted to stop it. Such files are now named as not applied because of the interrupt, with no resend instruction. The interrupt is also checked before the plan path checks, so a plan file skipped by an interrupt reports the interrupt rather than an unavailable workspace.
With file-level partial application, an edit_files result that is not
an error can still reject some files. Showing an args-derived diff for
those would present unwritten edits as applied.
A rejected file now shows its path and error instead of a diff. A
file whose outcome the server could not determine (status unknown)
shows the same way, labelled as possibly not applied rather than
unchanged. The header counts only applied files ("Edited 1 of 3
files"). A file missing from a partial result shows its path with
"No result reported for this file." and no diff, and is not counted
as applied.
Applied and older results only report success when every file was
written, so files they omit, and applied entries without a diff (from
agents that predate per-file diffs), keep the args-diff fallback. An
explicit empty diff is still a no-op edit with nothing to show.
Arg paths are trimmed before grouping, as the backend trims them
before running the edits, so they match the result paths.
This was referenced Sep 26, 2026
…es-partial-apply # Conflicts: # coderd/x/chatd/chattool/editfiles.go # coderd/x/chatd/chattool/editfiles_test.go # site/src/pages/AgentsPage/components/ChatElements/tools/utils.ts
The tool description said the other files are still applied, which reads as every other file; it now says files without errors. The interrupted-file comment claimed such a file has nothing to fix, though it was never checked. The single-file reasoning moves from the isAgentResponse doc to its only caller, where the rejected or unknown status is chosen.
…es-partial-apply # Conflicts: # site/src/pages/AgentsPage/components/ChatElements/tools/Tool.test.tsx
The unit 3 merge resolved a conflict in the parseEditFilesArgs doc comment to this branch's side, dropping the revised wording from the flat-args branch. Combine that wording with the path trimming note.
An unknown row showed sighted users only the path and a raw transport error, which reads as "not applied". It now starts with "Could not confirm whether these edits were applied." and shows the error below. Status meanings are defined once in the ServerEditResult doc, the args-diff fallback once in the renderer, and "args diff" is the one name for a diff built from the call's edits. Comments that repeated those docs are gone. The story's unknown error is now a transport error, which is what the backend passes through.
…es-partial-apply # Conflicts: # coderd/x/chatd/chattool/editfiles.go # site/src/pages/AgentsPage/components/ChatElements/tools/utils.ts
…tial results A rejected file with one edit read "a.go was not applied (edits[1] was not applied)". It now reads "a.go was not applied (edits[1])". The agent-error comment no longer says the agent "commits" a file, which reads as a Git commit here; it says the agent writes a temporary file and renames it into place.
…es-partial-apply # Conflicts: # site/src/pages/AgentsPage/components/ChatElements/tools/Tool.test.tsx
The merge from the flat-schema branch kept this branch's parseEditFilesArgs doc comment, dropping the revised wording from the flat-args branch. Combine that wording with the path trimming note.
Test names for the two older result forms name their field ("result
with ok: true", "result with status: applied"), matching the
result-format branch. The ServerEditResult doc no longer uses the
internal include_diff flag name. Partial-result fixtures quote the
current backend message, including edits[i] for one-edit files and the
sentence for a file with an unknown outcome.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
edit_filesnow applies each file on its own: files without errors are written, and files with any error are left unchanged. In dogfood, 89% of the valid entries thrown away with a failed batch (1,704 of 1,910) were collateral from anold_texterror in another file. Partial application would have applied files in 914 of 1,022 failed multi-file calls.is_errorfalse,status: "partial". Files that were not applied are listed first, with theiredits[i]indexes and the agent's error. The agent reports only the first failing edit per file without its index, so every index for that file is listed.status: "unknown", with an instruction to re-read it before resending. This status is not in the plan's C4. It follows the rule that nothing is reported as known unless it is known.Two different literal paths to the same real file (
/repo/./a.goand/repo/a.go, or a symlink) are now separate requests, because grouping uses the path as sent. The agent used to reject symlink aliases within one batch.The UI shows applied files with their diff and rejected or unknown files with their error, never a diff. The header reads "Edited 1 of 3 files" for partial results. Results from agents older than 2026-04-18, which have no per-file diffs, keep the args-derived diff.
Stack, merge in order:
edit_filesargsedit_filesgrouping helpersedit_filesto a flat edits listedit_filesoutcomes explicitlyedit_filesper fileImplementation plan
edit_files: flat schema, clear results, file-level partial application
Scope of this stack: parts 1 to 5. Parts 6 (previous-call guard) and 7 (re-analysis) are not being implemented. Without the guard no file can have the
already_appliedstatus, so thealready_appliedentries, counts and UI note below are out of scope.Direction (for approval)
Outcome. Fewer wasted model turns on
edit_files: fewer malformed calls, no valid work thrown away, and results that say exactly what happened.Observable end state
editslist; each edit carries its ownpath.edits[i]), the reason, and what to resend.filesform of the same edits, and execution uses exactly what the hook approved.Recommended direction. Replace the nested
files[].edits[]schema with a flat list (schema B), then build clearer results, file-level partial application, no-op rejection and a previous-call guard on top, in that order. All changes are in coderd; the workspace agent API stays as it is until the changes are validated.Why. Evidence from dogfood (75,070 calls, 8,914 failures, 2026-03-16 to 2026-09-26; analysis in
/home/coder/efa):filessent as a JSON string, 81% from Claude families; none parsed as-is. The nested array is the suspected cause.Shape of the work (in order)
filesargs keep rendering).Ruled out
{path, edits}hybrid or top-level first edit (proposal A): two ways to express an edit; unclear what applies when.\"in tests.filesshape after B: one contract for hooks and UI; after the old_text/new_text rename only 62 calls used old keys.editsraw to hooks: a policy readingedits[].pathwould see nothing.Decisions made
editssent as a JSON string is accepted only if it parses as-is; storage, UI, hooks and execution see the decoded array.{"files":[{"path","edits":[...]}]}and convertsinput_overrideback. Moving hooks to B is a separate, explicit break after B is validated (chatd is experimental, so this is allowed but not needed yet).filesshape is rejected with an error showing the B shape.is_errorfalse,status: "partial").edit_filescall in the chat; exact match; no trimming.Assumptions, constraints, tradeoffs
coderd/x/chatdis experimental; the hook contract could change, but this plan keeps it unchanged until B is validated.Risks
editsare broken like before. Evidence: the safety net only accepts strings that parse as-is, and none of the 2,681 historical stringifiedfilesvalues did; the top-level array is what gets stringified (files2,680 times vs the nestededitsarray 3 times), and B still has one. Trigger:claude-sonnet-5malformed rate stays near today's 27% two weeks after part 2. Response: analyze B-era failure shapes with the existing tooling; add repair rules only for observed shapes.User stories
Model
{"edits":[{"path","old_text","new_text"}]}with no nested groups.editsarrives as a JSON-encoded string that is otherwise valid, the call still works.filesshape, the error shows the new shape.User
Admin / hook author
pre_tool_usepolicies keep working unchanged when the model-facing schema changes.Contracts
C1. Tool input (schema B)
{"type":"object","required":["edits"],"properties":{ "edits":{"type":"array","minItems":1,"items":{ "type":"object","required":["path","old_text","new_text"],"properties":{ "path":{"type":"string","description":"Absolute path of the file to edit."}, "old_text":{"type":"string","description":"Exact text to replace. Must match exactly one location unless replace_all is true. Must differ from new_text."}, "new_text":{"type":"string","description":"Replacement text."}, "replace_all":{"type":"boolean","description":"Replace every match of old_text."}}}}}}Example:
{"edits":[{"path":"/repo/a.go","old_text":"x := 1","new_text":"x := 2"},{"path":"/repo/b.go","old_text":"foo()","new_text":"bar()"}]}C2. Input acceptance
editsis a JSON string that parses as-is into an array of objects: decoded where the tool call arrives (before hooks and persistence); everything downstream sees the array. Anything else in a string: whole call rejected.fileskey: whole call rejected, error shows the C1 example.old_text/new_textare accepted inside edits (nosearch/replace).edits[i].pathties its error to that file (for example relative path, empty old_text). An edit withoutpath, or malformed input, rejects the whole call. Plan-turn path restriction stays whole-call.C3. Application semantics
path(as sent), keeping their order; each file's edits apply in order.old_text == new_textis an error for its file.edit_filescall in the chat (most recent call before this one, with status applied or partial) defines the applied edits:(path, old_text, new_text, replace_all)of its applied files. Each edit in the new call that exactly matches one of them is skipped and reported as already applied; the rest of that file proceeds normally. Paths compare as sent by the model, not symlink-resolved.C4. Tool result (what the model reads)
All applied (
is_error: false):{"status":"applied","message":"Applied edits to 2 files.", "files":[{"path":"/repo/a.go","status":"applied","diff":"..."}, {"path":"/repo/b.go","status":"applied","diff":"..."}]}Partial (
is_error: false), rejected files first:{"status":"partial", "message":"Applied 1 file. 1 file was already applied by your previous call. /repo/c.go was not applied (none of edits[3], edits[4] were applied): fix and resend only the edits for /repo/c.go.", "files":[ {"path":"/repo/c.go","status":"rejected","edits":[3,4], "error":"old_text matches 3 occurrences (expected exactly 1). Include more surrounding context to make the match unique, or set replace_all to true"}, {"path":"/repo/a.go","status":"applied","diff":"..."}, {"path":"/repo/d.go","status":"already_applied","message":"edits[5] matched your previous call and was not applied again."}]}Nothing applied (
is_error: true), text. Part 3 (all-or-nothing, one agent request):From part 4 (per-file requests, every file failed):
Whole-call rejections (malformed input, old shape, missing path, plan restriction, infrastructure) use the same
is_error: truetext form, starting with what to change.Counting: the message counts applied, already-applied and rejected files separately; an already-applied file is not counted as applied.
statusisappliedwhen no file is rejected (including when some or all files are already applied),partialwhen at least one file is applied or already applied and at least one is rejected, and an error when no file is applied or already applied.C5. Workspace agent API (
POST /api/v0/edit-files)Unchanged. Part 3 sends one request with all files, as today; a 400 means nothing was written (batches are atomic since 2026-03-24). Part 4 sends one request per file (edits grouped by path, in order) and derives each file's status from its own response.
C6. Hooks
pre_tool_use.tool_inputforedit_fileskeeps today's shape:{"files":[{"path":"...","edits":[{"old_text":"...","new_text":"...","replace_all":false}]}]}, built from B by grouping edits by path in order (a string-encodededitsis decoded first).input_overrideis accepted in the same shape and flattened back to B before validation and execution.docs/admin/setup/chat-lifecycle-hooks.md: note thatedit_filestool input is presented in this grouped form rather than the model's exact bytes.C7. Storage and UI
filesargs in existing chats.status: applied shows the server diff; rejected shows its error and never a synthetic diff; already_applied shows a note; header reflects partial ("Edited 1 of 3 files", counting only applied files).C8. Observability
Implementation detail
Parts, surfaces, dependencies
utils.ts/Tool.tsx/EditFilesTool.tsxparse and render B args alongside oldfilesargs.coderd/x/chatd/chathooks(or next totoolinput.go): B to grouped form, grouped override to B. Unit tests only.coderd/x/chatd/chattool/editfiles.go(args, validation, grouping into the agent'sFileEdits, index mapping, tool description), chatloop tool-input decoding at stream arrival (prototype:RepairToolInputinterface, worktreemathias/edit-files-input-repair), wiring 2b intotoolinput.go/generation.go, hooks docs note.editfiles.gorendering (C4 applied and nothing-applied forms), UI. Depends on 2.editfiles.go(one agent request per file, per-file statuses, C4 partial form), UI statuses. Depends on 3.editfiles.govalidation (coderd side, before the agent call). Depends on 4.generation_preparer.go(find the previousedit_filescall and C4 result in the prompt rows loaded per generation; pass applied edits intoEditFilesOptions),editfiles.go. Depends on 4 (needs per-file statuses in stored results)./home/coder/efa(extract.py and shape classifier need B awareness and the raw-input field). After 2 and after 4.References
/home/coder/efa/SUMMARY.md,out/{errors,retries,models,safetynet,noop_partial,guard,guard_prev}/REPORT.md.docs/admin/setup/chat-lifecycle-hooks.md(lines 69-82).agent/agentfiles/files.go.Verification
editsis stored as an array;pre_tool_usereceives the grouped form; aninput_overridein grouped form executes as approved.statusfollows the C4 counting rule.claude-sonnet-527.3% of calls), collateral withheld entries (1,910), not-found after identical re-send, no-op follow-up rework (73% vs 51%).Invariants
input_overridetheedits[i]indexes in results follow the flattened override, not the model's original call.