Codex: Cache dynamic model records to prevent app hang - #781
Conversation
baron
left a comment
There was a problem hiding this comment.
Blocking on exact head 44a1a2d98753234f921ccd857e208cfad8711d3e.
CodexDynamicModelStore now has one process-global _cachedRecords, while save, load, modelOptions, and displayName still accept arbitrary UserDefaults domains (CodexAIModelCatalog.swift:313-351). Saving or loading suite A can make a later load(defaults: suiteB) return suite A's records without consulting suite B. That breaks the existing injected-storage authority and can cross-contaminate tests or alternate defaults domains.
Please scope the cache to the defaults domain (or cache only .standard) and add a two-suite regression proving isolation.
Audit disposition — correctness blocker (2026-08-14)
Scope the cache by defaults domain/identity, or cache only |
Deep-review assessment — 2026-08-14Disposition: blocking storage-authority bug. A process-global Scope the cache by a stable defaults-domain identity, or cache only |
This PR addresses an app hang (REPOPROMPT-H6) caused by repeated synchronous JSON decoding from
UserDefaultson the main thread.Problem:
During model sorting (e.g., in
AIModel.sortedForPicker),CodexDynamicModelStore.displayNamewas called repeatedly. Each call todisplayNameinvokedCodexDynamicModelStore.load(), which synchronously read and JSON-decoded the entire[CodexDynamicModelRecord]array fromUserDefaults. This O(N log N) operation, performed on the main thread, led to significant blocking and app hangs.Solution:
An in-memory cache (
_cachedRecords) has been introduced withinCodexDynamicModelStoreto store the decodedCodexDynamicModelRecordarray.Changes Made:
_cachedRecords: A private static variable_cachedRecords: [CodexDynamicModelRecord]?was added toCodexDynamicModelStoreto hold the cached records.cacheLock: AnNSLock(cacheLock) was added to ensure thread-safe access to_cachedRecords, preventing data races during concurrent read/write operations.load()optimization: Theload()method now first checks_cachedRecords(under lock). If records are present, they are returned immediately. On a cache miss, it proceeds to read fromUserDefaults, decode the JSON, and then populates_cachedRecords(under lock) before returning.save()cache update: Thesave()method now directly updates_cachedRecords(under lock) with the newly saved records after writing them toUserDefaults. This ensures the cache is always fresh and avoids an immediate re-decode after a save operation.Impact:
This change significantly reduces main thread blocking by ensuring that the expensive JSON decoding from
UserDefaultshappens at most once persave()cycle, rather than on every comparison during model sorting. This resolves the observed app hang.Fixes REPOPROMPT-H6