fix(recap): Right / Wrong / Fix on daily recap learnings no longer fails with "Couldn't save" when the memory was re-extracted
Daily recap learning cards could keep a deleted or superseded memory id. Right / Wrong / Fix then repeatedly failed with “Couldn't save, try again”. The owner's two requests at 19:29Z returned 404. The exact failing id was retired by conversation-source re-extraction; two committed one-to-one replacement receipts lead to a live memory.
Refresh learning references when an authenticated app fetches recap list, search or detail responses. Follow owner-scoped explicit successor links and unambiguous source replacements with exactly one target, use the live target's current text and id together, and deduplicate resolved cards. Omit retracted, missing or locked targets with no verified live successor. A replacement set with several new targets is not enough to guess which fact should receive feedback. Existing recap documents are not rewritten and deleted memories are not recreated.
This reaches installed apps after a server deploy and a fresh recap response. An already-open page or an old client cache keeps its old ids until refreshed. No new app build is required. Shared/public recap snapshots and generation validation do not hydrate current private memory content. All feedback mutations still go through the existing MemoryService authority.
The response resolver has a 12-hop cap, a response-local cache, bounded metadata/source-receipt RPCs and a five-second budget for starting additional hops. Provider failure ends further resolution for that response and emits shared fallback telemetry. The kill switch DAILY_RECAP_LEARNING_REFS_ENABLED defaults on; setting it false restores the prior read projection.
Read-only evidence (ro-prod): the fixed window 2026-11-14 19:37:48Z to 2026-11-15 19:37:48Z contains four sampled successful review requests and two 404s; no route-specific ERROR entries were returned. Successful request logs are sampled (the quality brief records roughly 10% for 2xx), so this is not a fleet rate or a distinct-user estimate. Owner shape census measured 20:04:20Z: 140 learning cards, 18 closed/unavailable; eight have verified live successors (three explicit links and five unique-target source replacements, including consolidation), nine have only ambiguous source-retraction sets, and one has no retained successor evidence. No ids or memory/recap text were printed.
Verification: 117 isolated affected/importer files (3609 passed, 3 skipped, 5 deselected), including 28 production-shape/HTTP regression cases; required premerge sweep (9 files, 0 failures); full installed pre-push PASS with 43 shared checks, 0 backend type errors, and 37 selected unit files (4283 passed, 86 expected failures, 1 deselected). The cross-service slow guard passed 33 with 3 skipped; final exact-head cross-service readback is recorded in the lane report. Suites overlap and their counts are not additive.
Not verified: deployed release identity, hosted CI, physical iPhone rendering or live feedback writes. No production writes, push, PR, merge or deploy in this lane.
Invariants: INV-MEM-1, INV-MEM-3, INV-MEM-4, INV-MEM-5. Historical reads retain MemoryService suppression/decryption/entitlement checks; metadata only locates a candidate and never authorizes a mutation. No similarity matching or bulk backfill.
Line-Count-Exception: backend/routers/users.py | 2802 -> 2806 | Wire the small owner-only response normalizer into the existing list, search and detail endpoints; new policy stays in recap_review.py.
Failure-Class: FC-permanent-review-conflict-reoffered
UI-Evidence: none -- backend only
Evidence and handoff: data/run-2026-11-15-quality-12h/q12-ao-recap-feedback.md (m5-mbp workspace); prepared 2026-11-15T20:02:43Z.
🤖 Generated with Claude Code