Repository navigation
fix(backend): restore the released 204 contract for conversation deletion - #21329
Open
aryanorastar wants to merge 6 commits into
Open
aryanorastar wants to merge 6 commits into
aryanorastar wants to merge 6 commits into
Conversation
…on deletion
DELETE /v1/conversations/{conversation_id} shipped to released mobile clients
as 204 No Content. The 2026-07-04 response_model backfill (e22938a) turned
it into a 200 with a StatusResponse body, and the released app
(app/lib/backend/http/api/conversations.dart::deleteConversationServer) gates
delete success on exactly 204 — so every conversation delete there reads as a
failure: the optimistic tombstone is dropped immediately, the server-cursor
rebase and fetch-revision bookkeeping are skipped, and once the server delete
cannot complete (e.g. cascade retraction failing closed) the deleted
conversation resurfaces on the next refresh. This is the recurrence of
Restore status_code=204 with no body (the folders/mcp convention) and pin the
contract from both sides in backend/tests/unit/test_conversation_delete_wire_contract.py:
the router declaration and the regenerated app-client OpenAPI spec.
Verified: offline harness — cascade delete of a conversation with attributed
memories returned 503 before this change, 204 with an empty body after.
Failure-Class: FC-privacy-delete-fails-on-unprovisioned-derived-store
Amp-Thread-ID: https://ampcode.com/threads/T-01a0b590-2123-744a-9e4a-16706295c2d3
Co-authored-by: Max Carter <undivisible@vk.com>
(cherry picked from commit 21002a8)
…t contract gate The gate treats the merge-base spec as the released-clients truth. That baseline can itself carry a spec-only regression: e22938a (2026-07-04) flipped DELETE /v1/conversations/{conversation_id} from the released 204 No Content to 200 + StatusResponse while every released mobile client still gates delete success on exactly 204 (app/lib/backend/http/api/conversations.dart::deleteConversationServer, BasedHardware#10446 recurrence: desktop delete errors, mobile re-adds on refresh). With the baseline poisoned, the gate blocked restoring the actual released contract, and the July regression shipped precisely because no guard existed to catch it. Add RELEASED_STATUS_RESTORES, patterned on DELIBERATELY_REMOVED_ENDPOINTS: an explicit, evidence-cited (route, method, status) record whose released-client contract predates the baseline regression, so restoring that status is not flagged as a new success status. Entries are narrow — an unrecorded operation cannot borrow the allowance — and both the allow-and-refuse directions are unit-tested. Evidence: app/lib/backend/http/api/conversations.dart:140 in the released app source returns response.statusCode == 204 for this call. Failure-Class: none Amp-Thread-ID: https://ampcode.com/threads/T-01a0b76d-bb38-749d-a0da-1b0837aa8e2e Co-authored-by: Max Carter <undivisible@vk.com> (cherry picked from commit 11cc3c5)
…e contract Regenerated from the updated app-client spec with backend/scripts/generate_ts_openapi_types.py and generate_swift_openapi_types.py rather than cherry-picking 2fb1f42 from BasedHardware#14691, whose generated files predate current main. Only the conversation delete function changes: Swift returns Void, TypeScript returns without parsing a body. No in-tree code calls either generated function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…contract test_conversation_tombstones.py (BasedHardware#21000) landed after the 200-with-body regression and asserted the handler's {'status': 'Ok'} return. The route is 204 No Content again, the contract the released mobile client gates on (app/lib/backend/http/api/conversations.dart::deleteConversationServer returns statusCode == 204), so the handler returns None. The tests' subject, tombstone ordering against retraction and cleanup, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ody over HTTP The wire-contract test pinned the route declaration and the published spec. This adds the HTTP layer: the real route through FastAPI's TestClient, with only storage side effects stubbed, for cascade=false and cascade=true. Against main's route both requests answer 200 (assert 200 == 204), which the released mobile client reads as a failed delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full reasoning lives in test_conversation_delete_wire_contract.py; the router stays at its line-count baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
|
Verified the core regression story end to end — this is a well-evidenced fix.
Checks are green on head (backend unit suite, hermetic merge gate, desktop Swift build & tests). Leaving formal sign-off to a human maintainer: this reverses a deliberately-merged API change on a user-data deletion endpoint (released 204 contract vs the backfilled 200), and it records a standing exception in the app-client compatibility gate — both deserve a maintainer's explicit call on which contract is canonical going forward. Thanks for the exceptionally thorough write-up and for preserving the cherry-pick provenance. by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
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.
What changed and why
Deleting a conversation on mobile does not stick: the app shows it deleted, then it reappears about 30 seconds later (Discord "Deleting Convos and Memories", the #10446 recurrence). The server delete succeeds; the client reads it as a failure.
DELETE /v1/conversations/{conversation_id}shipped to mobile as 204 No Content. The 2026-07-04response_modelbackfill (e22938a) turned it into a 200 with aStatusResponsebody, and the released app gates success on exactly 204 (app/lib/backend/http/api/conversations.dart::deleteConversationServerreturnsresponse.statusCode == 204). Every successful delete reads as failed, the app drops its optimistic hide, and the next refresh brings the conversation back.This restores the released contract. It is the mobile half of #14691, rebuilt on current
main: the two backend commits are cherry-picked with-xand keep their authorship; the desktop half of #14691 already landed as #15097. #14691 itself is far behindmainand red on an unrelated stale fixture.backend/routers/conversations.py:status_code=204, no body. Everythingmainadded to the handler since — tombstones (fix: prevent deleted conversations from backlog resurrection #21000), the product event — is kept.docs/api-reference/app-client-openapi.json: regenerated withexport_openapi.py --surface app-client; only this path changes.check_app_client_openapi_compatibility.py: records(/v1/conversations/{conversation_id}, delete, 204)as a restoration of the released status, with the client evidence cited, so the gate does not flag it as a new success status.Released-client compatibility
delete()usesperformVoidRequest, which accepts any 2xx and reads no body (checked at 0.12.447)omiApi.deleteis axios, which resolves on 2xx with an empty body; the released generated function already returnsundefinedfor 204Product invariants affected
none (
scripts/pr-preflight --suggest)How it was verified
BACKEND_UNIT_TEST_FILE_LIST bash test.sh.test_conversation_delete_wire_contract.py, 4 tests: the route declaration, the published spec, and the real route over HTTP through FastAPI'sTestClientforcascade=falseandcascade=true(storage side effects stubbed). Againstmain's route, the declaration test and both HTTP tests fail —assert 200 == 204.export_openapi.py --surface app-client --checkpasses on the regenerated spec, and both client generators report fresh with--check.Not verified
Not exercised against a deployed backend or on a phone. The offline harness needs Docker and a Typesense runtime this machine does not have; the HTTP behavior is proven through
TestClientwith the production route and FastAPI serialization.Tests
test_conversation_delete_wire_contract.py: the contract from three sides — declaration, spec, and HTTP response.test_conversation_tombstones.py: two assertions change from== {'status': 'Ok'}tois None. Those tests landed with fix: prevent deleted conversations from backlog resurrection #21000 against the 200-with-body regression; their subject, tombstone ordering against retraction and cleanup, is unchanged. External source for the new expected value: the released mobile client'sstatusCode == 204gate cited above.Failure class (fixes)
Failure-Class: none
The cherry-picked commit cites
FC-privacy-delete-fails-on-unprovisioned-derived-store, which covers the 5xx-on-unprovisioned-store mechanism fixed in #15097. This half is a different mechanism — a schema backfill changed a released success status — and no registered class describes it. It is guarded by the wire-contract tests and the recorded restoration in the app-client compatibility gate.UI evidence
UI-Evidence: none -- backend status code and regenerated API clients only; no screen, widget or copy changes
🤖 Generated with Claude Code