iOS-specific knowledge. Claude reads this before every task and updates it when discovering something new. For cross-cutting knowledge, see
../PROJECT_MEMORY.md.
Last updated: 2026-08-06
Root ../CLAUDE.md is normative; the full pre-pass iOS routing preamble is preserved.
Search the topic text below as subsystem keywords. Each link is mandatory when its row matches the task.
Save Draft adopts the visible AI suggestion synchronously: Topic 049.
| Topic / search terms | Detail |
|---|---|
| Key Files | read in full |
| Inbox list = ONE merged read-model (ADR-IOS-055, 2026-07-09 evening) — supersedes the same-day guard/carry-over fixes | read in full |
Summary recipient_status (cc detection) — 2026-07-04 |
read in full |
| IMAP external-deletion blind spot — old server-deleted messages linger forever (audit 2026-07-02) | read in full |
| CLI testing breaks with "Simulator device failed to launch / Launchd job spawn failed" → app is UNSIGNED in DerivedData (2026-06-22) | read in full |
Test flakiness: UserDefaults(suiteName:) isolation FALLS THROUGH to .standard — isolating a suite's WRITES is necessary but not sufficient (2026-06-22) |
read in full |
| Foreground-return UI freeze — main-actor DB ops must not block (2026-06-21) | read in full |
| OAuth / Google Cloud | read in full |
| Architecture Patterns | read in full |
| Email image aspect-ratio distortion — gated post-load correction (2026-06-26) | read in full |
| IMAP Connection Pool (supersedes ADR-IOS-014) | read in full |
| ProviderWorkQueue Cancellation Semantics (2026-06-09) | read in full |
| FTS Tokenizer — NO tokenchars (ADR-024, 2026-06-09, lockstep with Rust crate) | read in full |
| SwiftUI preview convention | read in full |
| Inbox usage-throttle banner (ADR-IOS-044) | read in full |
| Remote Search (SearchView) | read in full |
| FTS↔GRDB id drift (folder baked into the key) — "indexed but unfindable" / wrong-subject class | read in full |
| "Searchable but can't open / no snippet / not in its folder" — GRDB-side stuck headers (NOT FTS orphans), IMAP (under investigation 2026-06-22) | read in full |
| Stale-detection window MUST match the fetch's ordering dimension — IMAP Archive "missing months" data-loss (CONFIRMED ROOT CAUSE + FIXED 2026-06-23) | read in full |
| GRDB Persistence | read in full |
GRDB ValueObservation — DO NOT put on messageHeader |
read in full |
| AI Priority Processing (ADR-IOS-013) | read in full |
| IMAP Message IDs & UID Resolution | read in full |
| iOS Keychain Persistence | read in full |
| Persistent Offline Action Queue (ADR-IOS-003, ADR-IOS-018) | read in full |
| Progressive Backfill & Storage Budget (ADR-IOS-005, ADR-IOS-006) | read in full |
Backfill diagnostics: backfill.log channel (2026-07-02) |
read in full |
| Backfill stall fixes (2026-07-02) — UID-remap re-key, no fast-sync cursor reset, persisted server limit | read in full |
bodyComplete = FTS-indexed truth; display cache has NO flag (ADR-IOS-050, 2026-07-02) |
read in full |
Backfill / Fast Sync Completion — gate on pendingBodyCount, NEVER a server total |
read in full |
| Ever-Rolling FIFO Queues (ADR-IOS-027) | read in full |
| Bounded Memory (CRITICAL) | read in full |
| Optimistic UI Rollback | read in full |
| Gmail Label System | read in full |
| SwiftUI Observable Array Mutation Safety | read in full |
| SwiftUI Layout Gotchas | read in full |
| HTML Email Render Pipeline (AutoSizingHTMLView) — MUST stay idempotent (ADR-IOS-039) | read in full |
| Swift Gotchas | read in full |
| Folder.== must include every UI-visible field | read in full |
| IMAP Folder Role Detection & Dedup (iCloud "Trash" + "Deleted Messages") | read in full |
| Two-Tier Sync (ADR-IOS-009) | read in full |
syncStartup Budget Discipline (v50/v51 lesson) |
read in full |
| Stale Message Detection (syncMessages) | read in full |
| Benign Sync Log Noise — DraftUpsertSkip + Gmail 429 (diagnosed 2026-06-09) | read in full |
Transient provider HTTP errors must NOT surface as a sync failure — SyncEngine.isTransientError (2026-06-17) |
read in full |
Raw NIO error types must be added to isConnectionError by NSError DOMAIN, not substring (2026-07-04) |
read in full |
| ICS Calendar Import — Invisible SFSafariViewController Hack | read in full |
| Attachment Support | read in full |
| Compose AI Suggestion — chat pill edits suggestion in place when bubble is visible | read in full |
| Compose body — caret-aware scroll, NO input gating | read in full |
| Compose Attachments | read in full |
| SwiftMail Types | read in full |
| Outgoing email header encoding — RFC 2047 (mojibake fix, 2026-06-21) | read in full |
| Hybrid FTS5 + Vector Search (Local) | read in full |
| Device Prompt Sync & AI Cache Probe | read in full |
| Cross-Instance Action Tag Sync (ADR-IOS-036, supersedes ADR-IOS-004 "First Compute Wins") | read in full |
| AI Summary & Action Pipeline | read in full |
| Background AI Processing (Tier 3) | read in full |
| Swift 6 BGTask Isolation Pattern (ADR-IOS-020) — CRITICAL | read in full |
| SSE Streaming & Tool Execution Loop | read in full |
| Thread (ThreadGroup) Actions — tag-tap dispatch & grouped undo | read in full |
| Manual Tag Teaching (Long-Press Context Menu) | read in full |
| Tool Registry (Scaffold) | read in full |
| Agent Chat (ADR-IOS-022, ADR-IOS-023) | read in full |
| Screen Keep-Awake (chat pill) | read in full |
| Agent Compose FIFO Queue (ADR-IOS-030) | read in full |
| Outgoing Threading — reply/forward stay in-thread on every provider (ADR-IOS-043, 2026-06-23) | read in full |
Incoming Thread Detection — ThreadDetection.findRelatedMessages has NO subject-based fallback (by design) |
read in full |
| Outbox — Persistent Offline Send Queue (ADR-IOS-019) | read in full |
| Proactive Local Notifications (ADR-IOS-026) | read in full |
| App Icon Badge Routine (NSE counter + main-app recount) | read in full |
| Persistent NSE log file + watchdog partial-result delivery (2026-07-09) | read in full |
| Sync-status subtitle ("Updated N min ago") | read in full |
Stuck-isLoading rule: async view loads must defer-clear their spinner flag — and .task must NOT hang on the conditional it flips |
read in full |
| Cron Reminders (ScheduledItem Architecture) | read in full |
| Banner Flash Prevention (MailNavigationView) | read in full |
Test Isolation for UserDefaults-Backed State |
read in full |
| Delivered-Notification Cleanup | read in full |
Startup Data Migrations (StartupMigrations.swift) |
read in full |
| TextEditor/TextField Caret Jump — Never Save Per Keystroke (2026-07-02) | read in full |
| Migration Splash / iOS Indeterminate Linear ProgressView Pitfall (2026-07-01) | read in full |
| Zero (BYOK) Plan — IAP Surface (ADR-IOS-040) | read in full |
| BYOK Model Picker — Two-Section Query-All (ADR-026, 2026-07-08) | read in full |
| HTML-Document Guard for text/plain Extraction (2026-06-11) | read in full |
| GRDB Database Suspension — 0xdead10cc Defense (ADR-IOS-041, 2026-06-12) | read in full |
| Dictation leaves AVAudioSession active → haptics die process-wide (2026-06-27) | read in full |
| fullScreenCover content must never re-fetch by a rekeyable id → presented-but-empty cover renders SOLID BLACK (2026-07-08) | read in full |
| Inbox gesture actions derive intent from the VISUALIZED snapshot, never a gesture-path DB read (dead-toggle fix, 2026-07-10) | read in full |
| Action queue coalesces gesture intents to latest-per-field (ADR-IOS-057, 2026-07-10) + overlay call-site audit closed | read in full |
| Knowledge Gaps | read in full |
These preserve source history; read them when a current topic, ADR, plan, or shipped-release comparison points to the older design.
| Status | Topic / search terms | Detail |
|---|---|---|
| Historical | Original project-memory preamble | read in full |
Pre-v3-line topics absent from v1.6.38:PROJECT_MEMORY.md; bodies byte-for-byte, provenance in ported-manifest.tsv.
| Status | Topic / search terms | Detail |
|---|---|---|
| Historical | HISTORICAL — Intention journal + fold-at-drain (ADR-IOS-058; superseded by ADR-IOS-060) | read in full |
| Current | Intention queue V2 — authoritative current direction (ADR-IOS-060, 2026-07-13) | read in full |
| Current | FakeIMAPServer's wrongMessageViolations oracle is BLIND when target and bystander share an RFC 822 Message-ID — C3 goes vacuous; pin on the UID STORE/EXPUNGE log |
read in full |
Authored after v1.6.38, so deliberately not rows in manifest.tsv; provenance, source line ranges and per-fragment sha256 in amendments-manifest.tsv.
| Status | Topic / search terms | Detail |
|---|---|---|
| Historical | Compaction drift list — a post-v1.6.38 amendment can differ from its routed twin; check the twin BEFORE editing |
read in full |
| Current | v3 provider-id action-queue forward-port — ✅ SHIPPED v1.7.0; v3 DELETED, main IS that line; R0/R3 RETIRED; ⛔ v2final never shipped |
read in full |
| Current | T1.3 — a NEW gesture fails CLOSED on an unknown UIDVALIDITY epoch: newGestureRefusedForUnknownEpoch is a silent no-op (IOS-EPOCH-001, C3) |
read in full |
| Historical | T4.S6 — ⛔ SUPERSEDED v3 intermediate, never implement from it: v69/v72 draft epoch stamps; RETRACTED mailbox-wide EXPUNGE |
read in full |
| Current | IMAP external-deletion blind spot — server-deleted messages linger; FIXED by ADR-IOS-051 Ph1+2: SyncEngineDeletionReconcile, handleVanishedUIDs |
read in full |
| Current | Persistent NSE log + watchdog partial delivery: NSELogStore/nse.log, PartialSignalHolder; idle-timer-vs-SSE; OneShotFlag.hasFired() |
read in full |
| Current | Two-instant wake handoff — "elapsed" means DO IT NOW: AccountManager.wakeUpDelay, holdUntil, IOS-OUTBOX-005, UInt64(negative) traps |
read in full |
| Current | isDeletedOnServer — FOUR materialisation paths plus a FIFTH PRESENTATION path (SearchView.searchAccount), IOS-IMAP-001 |
read in full |
| Current | 967e5b3c5, not one — deletion family + CalDAVProvider.splitSeries cap PUT; membership = no per-item recovery, NOT the verb |
read in full |
| Current | 🚨 await dbPool.read is NOT a short suspension — the ASYNC overload first awaits NSEDataBridge.mergeIfStagingPending() (7.6 s cold boot) |
read in full |
| Current | 🚨 a latch that AUTHORISES a transition must be HELD across the write — reading isDrainingOutbox then awaiting proves nothing (IOS-OUTBOX-006) |
read in full |
| Current | 🚨 a bare print is NOT production observability on iOS — stdout is DISCARDED on device; use BackgroundSyncLogger.logError; a RESIDUAL RECORD is an absolute |
read in full |
| Current | 🚨 a filter applied AFTER a query's LIMIT narrows the page instead of selecting it — InboxListReader.gather (IOS-SCROLL-002, IOS-BACKFILL-001) |
read in full |
| Current | 🚨 a staging key that names an ADDRESS must re-prove identity before reusing payload — nse_processed_message PK holds a UID (IOS-NSE-005, C3) |
read in full |
| Current | 🚨 a Swift String comparison does NOT reproduce SQLite BINARY collation — use utf8.lexicographicallyPrecedes; InboxOrdering, IOS-SCROLL-002 |
read in full |
| Current | 🚨 THE SAME STAGING KEY HAS FOUR WRITERS, AND "SAFE" POINTS THE OPPOSITE WAY FOR THREE (IOS-NSE-006, stagedIdentityPositivelyDiffers) |
read in full |
| Current | 🚨 AN ENUM WITH NO SILENT CASE DOES NOT PREVENT A SILENT PATH — ResultTapOutcome's "cannot reintroduce silence" comment was FALSE (case …: break) |
read in full |
| Current | 🚨 THE ADDRESS PROBLEM HAS TWO ADDRESS SPACES — Graph's /move response IS the COPYUID; ExchangeProvider.moveMessage discarded the new id (IOS-GRAPH-002); the retirement handoff now also re-addresses the QUEUE and follows the ROW — readdressQueuedOperations, accountScopedIdAccountIds (account-scoped ≠ immutable), markQueued, liveOperation, Outlook drain lanes, IOS-GRAPH-005 / ADR-IOS-081 |
read in full |
| Current | 🚨 uidValidityResetPendingAt STAYS ARMED ON PURPOSE — never demand proof of transience; crawlWalkWriteAllowed was the LAST consumer writing under it (16ecafd93) |
read in full |
| Current | 🚨 BOTH UIDVALIDITY RE-DRIVE OWNERS ITERATED syncableFolders (fullSync + imapDeltaSync) — an armed CUSTOM NON-FAVOURITE folder had NO re-drive: 145,754 rows |
read in full |
| Current | ✅ KNOWN_ISSUES.md APPEND PATH — RESOLVED 2026-08-12 — strip-before-compare KNOWN-ISSUES-AMENDMENT-BEGIN/-END + KnownIssues/Amendments/; IOS-IMAP-015 |
read in full |
| Current | 🚨 ATTACHMENT FILENAMES REJECTED, NOT REDUCED — AttachmentFilename.isSafeFileComponent; 255 NFD UTF-16 unit path-component cap (NOT bytes, NOT Characters) |
read in full |
| Current | SwiftMail PR #208 MOVE post-completion contract — only the COPYUID-bearing form is non-retryable (retain COPYUID, reconcile both folders); moveFailedAfterPossiblePartialCompletion — any tagged NO with no COPYUID, e.g. raw-channel NO No mailbox selected — is a retryable refusal since #115 |
read in full |
| Current | 🚨 "TRIAL ENDED" IS DERIVED, NEVER A NEW /whoami FLAG — has_subscription:false + the trial KEY present; .active REQUIRES plan_tier == "Trial"; AccountInfo.trialState(now:), AISubscriptionGate.trialHasEnded; #55 |
read in full |
| Current | 🚨 POST-LOGIN ROUTING WAITS FOR AN AUTHORITATIVE /whoami (issue #56) — PendingPlanNavigationLatch / pending_plan_navigation, AISubscriptionGate.lastAuthoritativeApplyAt, signInGeneration/applyIfCurrentEpoch |
read in full |
| Current | 🚨 AFFORDANCE LIFETIME COMES FROM THE DURABLE HOLD, NEVER FROM WHEN THE UI APPEARED — undo-send anchor drift = presentation latency Δ; same-name-different-instant queuedAt (#76) |
read in full |
| Current | 🚨 ONE LOG FILE PER PROCESS (#83) — AppLogStore/tabmail.log, NSELogStore/nse.log |
read in full |
| Current | 🚨 A DURABLE WRITE TO A MIRRORED IDENTITY COLUMN MUST REFRESH THE NSE MIRRORS — nse.accountMap/nse.imapAccounts are the extension's ONLY resolver; addIMAPAccount/addICloudAccount refreshed neither, so handleIMAPReconnect early-returned; mirrorAccountIdentity(); IOS-NSE-008 |
read in full |
| Current | 🚨 ORDINARY SIGN-OUT RELEASES THE DEVICE PUSH REGISTRATION AND ENDS THE AUTH SESSION — TabMailAuthService.signOut(): unregisterDeviceForSignOut() FIRST, then GoTrue logout?scope=local, BOTH legs always; guard getSession()?.userId == subject; push#42; SignOutHandshakeTests |
read in full |
| Current | Background credential refresh shares grant ownership — ProviderCredentialStore, CredentialRefreshGate, TabMailSessionRefresh; NSE cancellation, rotation, and foreground repair (#129) |
read in full |
| Current | SwiftMail upstream sync, exact app/NSE revision, #215/#216/#219 replacements, NIO IMAP #849 release dependency, parked #103/#74 and migration numbering on resume; 2026-09-09 fixes — R1 quadratic RFC 2231 continuations + C2 blank/windows-1252 charset filename loss are upstream #230 regressions fixed on fork branch fix/rfc2231-extended-parameters (upstream PR), C1 encoded display names (#226 contract) decoded app-side in EmlParsing.parse via RFC5322Parse.decodeRFC2047 |
read in full |