Skip to content

fix: propagate diff timeouts and declared diff errors to clients - #72

Merged
akonwi merged 2 commits into
mainfrom
fix/diff-error-propagation
Oct 11, 2026
Merged

akonwi merged 2 commits into
mainfrom
fix/diff-error-propagation

Conversation

@akonwi

@akonwi akonwi commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Fixes #69. Fixes #70.

Follow-up to #71. When a diff failed, clients saw "daemon returned malformed diff error" instead of the actual failure. Two bugs on either side of the wire caused this; each has its own commit.

#69: deadline expiry escaped as a generic internal error

ffbeaf2e fix(workingdiff): report observation deadline as a typed limit

  • When the service's 8 s observation budget expired, the raw context.DeadlineExceeded came back from roughly 48 sites in internal/workingdiff. The server mapped it to internal.
  • Each exported workingdiff method (Observe, ListTargets, ObserveTarget, ReadFile, ReadLineRange, ReadFileForAnnotation) now defers projectDeadline. That converts expiry of the service's own budget into limit_exceeded with details.limit: "deadline", the shape ADR 0023 already declares and DiffError.Validate already accepts.
  • If the caller cancels, or the caller's own deadline expires, the context error is returned unchanged ("cancellation remains cancellation").
  • The budget is now a Service field (default observationBudget), so tests can exhaust it deterministically.
  • No wire change. limit_exceeded with limit: "deadline" was already declared for every diff operation, so the OpenAPI, Swift sources and protocol version are unchanged.
  • Behavior change: annotation evidence reads that time out now get a typed limit error instead of a raw context error.

#70: the Go client reported declared errors as malformed

f93d5ab8 fix(api): project declared diff errors without reporting them malformed

projectDiffError built a protocol.DiffError from every API error and reported any validation failure as malformed. That hid two kinds of legitimate replies:

  • Generic declared codes (internal, invalid_request, and the common unauthorized-style rejections), as described in the issue.
  • Every diff error with required details (limit_exceeded, unsupported_repository, capacity_exceeded). This is not in the issue. The transport decodes declared details into TypedDetails, but the projection read only the untyped Details map, so validation always failed. Without this fix, the Diff timeouts reach clients as generic internal errors #69 deadline error would still have reached Go clients as "malformed".

The fix:

  • Only diff error codes become *protocol.DiffError. Typed details are flattened into its details map through their JSON field names.
  • Every other declared code goes through the ordinary ServerError projection. Undeclared codes are already rejected by the transport.
  • Malformed diff details are still rejected.
  • Adds DiffErrorCode.Valid(), matching ScratchpadErrorCode.Valid(). DiffError.Validate now uses it. This is Go-only; there is no wire change.

Validation

  • workingdiff: with an exhausted budget, list targets, observe target, observe working tree, and read committed file each return the exact typed deadline error, which passes DiffError.Validate. With the projection disabled, all four return the raw context.deadlineExceededError.
  • Caller context: cancellation and an expired caller deadline are returned unchanged.
  • Server route: a deadline limit is served as HTTP 413 with the exact limit_exceeded body.
  • Go client: real error bodies decoded by the diff operation's decoder, then projected. The exact type, code, message and details are checked for deadline, unsupported repository, capacity, stale target, unavailable, internal, invalid_request and unauthorized. 6 of these 8 were reported as malformed before the fix. A case with invalid details is still rejected.
  • Commands: gofmt -l ., go build ./..., go vet ./..., go test ./..., go test -race ./internal/workingdiff/ ./internal/server/ ./api/....

Client impact

  • The TUI shows "the diff took longer than the server allows" for timeouts, and "Could not load diff" for internal.
  • macOS reads only the error code, so it was not affected. It shows its existing limit_exceeded text for timeouts.

When a diff operation exhausted the service's 8 s observation budget, the
raw context.DeadlineExceeded escaped the service, so the server projected
it as a generic internal error and clients could not explain it.

Exported workingdiff methods now convert expiry of the service's own
budget into limit_exceeded with limit "deadline", the shape ADR 0023
declares. Cancellation or expiry of the caller's context is returned
unchanged, so cancellation remains cancellation. The budget is a service
field so tests can exhaust it deterministically.

Fixes #69
projectDiffError built a protocol.DiffError from every API error and
reported anything that failed DiffError.Validate as "daemon returned
malformed diff error". That hid two classes of legitimate replies:

- generic codes diff operations declare, such as internal and
  invalid_request, plus the common unauthorized-style rejections;
- diff errors with required details (limit_exceeded,
  unsupported_repository, capacity_exceeded), because the transport
  decodes declared details into TypedDetails while the projection read
  only the untyped Details map.

Only diff error codes are now projected as DiffError, with typed details
flattened into the DiffError details map; every other declared code goes
through the ordinary ServerError projection. Malformed diff details are
still rejected. Adds DiffErrorCode.Valid alongside the existing
ScratchpadErrorCode.Valid.

Fixes #70
@akonwi
akonwi merged commit 85585c6 into main Oct 11, 2026
4 checks passed
@akonwi
akonwi deleted the fix/diff-error-propagation branch October 11, 2026 01:14
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.

Go client reports declared generic diff errors as "malformed diff error" Diff timeouts reach clients as generic internal errors

1 participant