Add structured ApplyFileDiffsResult.Failure types (QUALITY-1264) - #351
Add structured ApplyFileDiffsResult.Failure types (QUALITY-1264)#351warp-agent-staging[bot] wants to merge 1 commit into
Conversation
Extend ApplyFileDiffsResult.Error to carry structured failures while keeping the 'message' field for backward compatibility. Add a new Failure oneof with variants: UnmatchedDiffs (with SearchBlockFailure per-block details), ChangesAlreadyApplied, MissingFile, ReadFailed, AlreadyExists, MultipleFileCreation, MultipleFileRenames, MutatedDeletedFile, no_diffs_applicable, remote_file_operations_unsupported, and Opaque. All user-content string fields are marked (sensitive). Regenerate Go bindings. Co-Authored-By: Oz <oz-agent@warp.dev>
There was a problem hiding this comment.
Overview
Extends ApplyFileDiffsResult.Error with repeated Failure failures = 2 alongside the preserved back-compat string message = 1, and adds a Failure message whose oneof kind covers all 11 categories from PRODUCT.md with nested UnmatchedDiffs / SearchBlockFailure / per-path types. The contract is wire-safe, matches the approved TECH.md snippet almost verbatim, and the checked-in Go bindings are in sync; the only findings are documentation-level.
Concerns
The expected_start_line / expected_end_line sentinel deserves a stronger comment, flagged inline. Because this file is edition = "2023", plain uint32 still carries explicit presence, so the generated Go bindings expose HasExpectedStartLine() / ClearExpectedStartLine() — while the Rust bindings are produced by rewriting edition = "2023" to syntax = "proto3" in apis/multi_agent/v1/gen/rust/build.rs, which drops scalar presence entirely. Presence is therefore only expressible on the Go side, 0 is the only portable "unknown" signal, and a Go consumer reaching for the generated Has*() accessor would be building on a contract the Rust client cannot honor. Both current consumers do the right thing (client encodes 0, server checks > 0), so this is a future-proofing ask rather than a live bug.
Relatedly, the PR description does not mention that this ships a 0 sentinel where TECH.md specified optional uint32. The deviation is justified — the optional label is not valid under protobuf editions, so the spec's literal form would not compile — but this is precisely the cross-repo agreement point most likely to drift later, and it should be called out in the description rather than left for a reader to reconstruct.
Regeneration rewrote the recorded protoc version from v7.35.1 back to v5.29.3 across 15 otherwise-untouched .pb.go files. CI ignores these lines via its git diff -I '^[[:space:]]*//.*(protoc|protoc-gen-go)[[:space:]]*v[0-9]' filter so nothing breaks, but it moves the repo's recorded toolchain backwards and the next contributor regenerating on a current brew protobuf will flip all 15 lines again. Reverting those version-comment lines before merge would keep the diff to the one file that actually changed.
Verdict
Found: 0 critical, 0 important, 3 suggestions
Checked directly: gh pr checks reports 6/6 green including Check Generated Code; regenerating locally with protoc 30.2 and applying CI's exact -I filter yields zero meaningful drift, confirming the committed bindings match the .proto; go build ./... and go vet ./... pass in apis/multi_agent. Field message = 1 is preserved byte-for-byte and no existing tag in ApplyFileDiffsResult is reused or renumbered, so all four PRODUCT compatibility criteria (10-13) hold on the wire. (sensitive) = true is present on every user-content string — Error.message, UnmatchedDiffs.file, SearchBlockFailure.search, all seven per-path file fields, and Opaque.message — satisfying PRODUCT invariant 7, and SearchBlockFailure.truncated satisfies invariant 8. Testing-exempt as a pure schema change; the PR body names the category and states the rationale, which is the correct treatment here.
Approve (posted as a comment review: GitHub rejects APPROVE because this PR was opened by the same factory app identity that is reviewing it — the verdict is accepted, a human still merges.)
Review run
https://oz.staging.warp.dev/runs/019fa63c-760c-749f-84ae-94e588e768af
| // 1-indexed inclusive start line the block was expected to match. | ||
| // 0 means unknown / not applicable. | ||
| uint32 expected_start_line = 3; | ||
| // 1-indexed inclusive end line the block was expected to match. | ||
| // 0 means unknown / not applicable. | ||
| uint32 expected_end_line = 4; |
There was a problem hiding this comment.
💡 [SUGGESTION] This file is edition = "2023", where plain uint32 still has explicit presence — the generated Go bindings expose HasExpectedStartLine()/ClearExpectedStartLine(). The Rust bindings, however, are produced by rewriting edition = "2023" to syntax = "proto3" in apis/multi_agent/v1/gen/rust/build.rs, which drops scalar presence, so the Rust client cannot distinguish unset from 0. Presence is thus only expressible on one side of the wire and 0 is the only portable unknown signal. Both consumers currently agree (client encodes 0, server checks > 0), but nothing in the comment stops a future Go consumer from switching to Has*() and silently diverging. Say so explicitly here.
| // 1-indexed inclusive start line the block was expected to match. | |
| // 0 means unknown / not applicable. | |
| uint32 expected_start_line = 3; | |
| // 1-indexed inclusive end line the block was expected to match. | |
| // 0 means unknown / not applicable. | |
| uint32 expected_end_line = 4; | |
| // 1-indexed inclusive start line the block was expected to match. | |
| // 0 means unknown / not applicable. | |
| // | |
| // Use 0 -- not field presence -- as the "unknown" signal. This file is | |
| // edition 2023, so the Go bindings expose HasExpectedStartLine(), but | |
| // the Rust bindings are generated by rewriting the edition to | |
| // syntax = "proto3" (gen/rust/build.rs), which drops scalar presence. | |
| // Presence is therefore not portable across consumers; 0 is. | |
| uint32 expected_start_line = 3; | |
| // 1-indexed inclusive end line the block was expected to match. | |
| // 0 means unknown / not applicable. See expected_start_line above. | |
| uint32 expected_end_line = 4; |
Summary
Add structured
Failuretypes toApplyFileDiffsResult.Errorintask.proto, enabling server-controlled wording ofedit_filesfailures.Part of QUALITY-1264 — "Structured apply-diff errors: server-controlled edit_files failure messaging." This is the proto contract PR (Agent P), defining the wire format that the client (Agent C) and server (Agent S) will implement against.
What changed
Extended
ApplyFileDiffsResult.Errorto carry structured, typed failure data alongside the existing back-compatmessagestring:Added
Failuremessage withoneof kindcovering all 11 failure categories:UnmatchedDiffs— file + fuzzy_match_failure_count + per-blockSearchBlockFailuredetails (search text, truncated flag, expected line range)ChangesAlreadyApplied,MissingFile,ReadFailed,AlreadyExists,MultipleFileCreation,MultipleFileRenames,MutatedDeletedFile— each carries the file pathno_diffs_applicable,remote_file_operations_unsupported—google.protobuf.EmptyOpaque— prerendered message for legacy/uncategorized failuresAll user-content string fields are marked
(sensitive) = true(PRODUCT invariant 7).Validation
This is a pure schema/contract change — no behavioral test exists.
(sensitive)audit: every user-content string field is marked(sensitive) = true:Error.message,UnmatchedDiffs.file,SearchBlockFailure.search, all per-file variant.filefields, andOpaque.message.go build ./...passes on the generated bindings../script/generate -a multi_agent -v v1produces no meaningful diff (only a protoc version comment change, which CI ignores).build.rs; no artifacts to commit.Compatibility
The
messagefield (field 1) is preserved for back-compat:message→ identical behavior to todayfailuresfield, readsmessage→ identical behaviorfailures, ignoresmessage(QUALITY-1264 goal)Originating thread
Slack thread
Conversation: https://staging.warp.dev/conversation/c113b5ca-c1de-4d63-a518-3b4f8d80e113
Run: https://oz.staging.warp.dev/runs/019fa5f3-f83d-788f-a65f-7bf202c66ddd
This PR was generated with Oz.