CHANGELOG-PROPOSAL.txt
3 weeks ago
QA-LOOP.md
3 weeks ago
RELEASE-QA-NIGHT.md
3 weeks ago
STUDY-CONSOLIDATION.md
3 weeks ago
STUDY-MCP.md
1 week ago
STUDY-SETTINGS-STORAGE.md
1 week ago
STUDY-WORKSPACE.md
3 weeks ago
STUDY-WPAI-LEARNINGS.md
3 weeks ago
STUDY-SETTINGS-STORAGE.md
122 lines
| 1 | # Settings storage: making saves snappier and safer |
| 2 | |
| 3 | Not scheduled. Written 2026-08-03 after a support ticket (Galit, goldlifeaxis.com) where |
| 4 | chatbot settings appeared to save and then reverted on refresh. Each stage below is |
| 5 | shippable on its own, in order, and each is useful without the ones after it. |
| 6 | |
| 7 | ## Where we are |
| 8 | |
| 9 | Everything lives in a single WordPress option, `mwai_options`, and the REST route |
| 10 | `settings/update` **replaces the whole array**. Three consequences: |
| 11 | |
| 12 | - **Last writer wins, silently.** A second admin, or the same admin with an old tab open, |
| 13 | saves a stale snapshot over everything. Nobody is told. This is the likely cause of the |
| 14 | ticket above: the user did not lose one setting, they lost the environment, the knowledge |
| 15 | env and the instructions all at once, which is what a whole-object overwrite looks like. |
| 16 | - **The blast radius is the whole configuration**, including API keys. A one-key POST once |
| 17 | wiped every environment and key on a test site of ours. If it can catch us, it will catch |
| 18 | an integrator. |
| 19 | - **It is large.** 559 KB on a normal site. `autoload` is off, so it is not read on every |
| 20 | page load, but it is serialized, sent and written in full on every save. |
| 21 | |
| 22 | ## What is actually in those 559 KB |
| 23 | |
| 24 | Measured on a real site, top-level keys by serialized size: |
| 25 | |
| 26 | | Key | Size | Share | |
| 27 | |---|---|---| |
| 28 | | `ai_engines` | 254 KB | 47% | |
| 29 | | `ai_models` | 214 KB | 40% | |
| 30 | | `ai_models_usage_daily` | 18 KB | 3% | |
| 31 | | `ai_usage_daily` | 17 KB | 3% | |
| 32 | | `ai_models_usage` + `ai_usage` | 21 KB | 4% | |
| 33 | | everything else (117 keys) | 35 KB | 3% | |
| 34 | |
| 35 | `ai_engines` holds 11 engines, and 252 KB of its 254 KB is the `models` array hanging off |
| 36 | each one. `ai_models` is a flat catalog of 430 model definitions. So **about 86% of the |
| 37 | settings blob is a model catalogue**, not settings: data we either ship in |
| 38 | `constants/models.php` or fetched from a provider and cached here. |
| 39 | |
| 40 | The real user settings are roughly 35 KB. Everything else is cache and counters riding along |
| 41 | on every save. |
| 42 | |
| 43 | ## Stage 1: shrink it (biggest win, lowest risk) |
| 44 | |
| 45 | Move the catalogue out of `mwai_options` into its own option(s), rebuilt on demand: |
| 46 | |
| 47 | - `ai_engines[*].models` and `ai_models` move to something like `mwai_models_cache`. |
| 48 | - Usage counters (`ai_usage*`, `ai_models_usage*`) move to their own option, or better to a |
| 49 | table, since they are append-heavy and have nothing to do with configuration. |
| 50 | |
| 51 | That takes the settings row from ~559 KB to ~35 KB. Saves get roughly 15x smaller, the |
| 52 | window for a partial write shrinks with it, and losing the cache costs a refetch rather than |
| 53 | a configuration. |
| 54 | |
| 55 | **No REST contract change**, so nothing outside the plugin notices. Do this one first. |
| 56 | |
| 57 | ## Stage 2: merge on write instead of replacing |
| 58 | |
| 59 | `settings/update` should merge the payload into the stored array key by key, so a client can |
| 60 | send only what changed. |
| 61 | |
| 62 | - Kills the stale-tab class outright: an old tab that only touches `chatbot_defaults` can no |
| 63 | longer erase `ai_envs`. |
| 64 | - Makes the REST API safe for integrators, who currently have to read-modify-write the whole |
| 65 | object and usually do not know it. |
| 66 | - **Compatibility:** a full object must keep working, because that is what every existing |
| 67 | client sends. Merging a complete payload is identical to replacing it, so this is |
| 68 | backwards-compatible by construction. The only true removal case (a user deleting the last |
| 69 | AI environment) needs an explicit signal, for example a `replace: true` flag or an explicit |
| 70 | empty array for that key. Decide this before writing code. |
| 71 | |
| 72 | ## Stage 3: refuse stale saves |
| 73 | |
| 74 | Merging fixes the tab that touched a different section. It does not fix two people editing |
| 75 | the same section. For that, send a version with the settings: |
| 76 | |
| 77 | - The GET returns a hash of the stored array. The client sends it back on save. |
| 78 | - If it does not match what is stored, reject with a clear error and let the UI say "this |
| 79 | page is out of date, reload before saving". |
| 80 | |
| 81 | Small, and it turns a silent loss into a visible, understandable refusal. |
| 82 | |
| 83 | ## Stage 4: move credentials out |
| 84 | |
| 85 | `ai_envs` (API keys) into its own option. After stages 1 to 3 this is belt and braces, but it |
| 86 | means no settings mishap can ever cost someone their keys, which is the failure people |
| 87 | actually remember. |
| 88 | |
| 89 | ## Backups |
| 90 | |
| 91 | Worth doing early, and independently of the rest: keep the last **three** snapshots of |
| 92 | `mwai_options`, rotated on write. |
| 93 | |
| 94 | - Write the current value to `mwai_options_backup_1..3` before each save, rotating, all with |
| 95 | `autoload` off. |
| 96 | - Surface them in Dev Tools: "Restore settings from 2 saves ago", with the timestamp and the |
| 97 | size. |
| 98 | - Cheap, and it converts an entire class of support ticket ("my settings vanished") into a |
| 99 | one-click recovery, whatever the cause. It also derisks stages 1 to 4. |
| 100 | - Skip writing a backup when the value is unchanged, so a save that touches nothing does not |
| 101 | push the useful history out. |
| 102 | |
| 103 | ## Rules while doing any of this |
| 104 | |
| 105 | - **The REST contract is sacred.** `settings/update` must keep accepting a complete object |
| 106 | and behaving as it does today. |
| 107 | - **Migrations must be idempotent**, and must not lose data if a site is on an old version, |
| 108 | reverts, or runs the migration twice. |
| 109 | - **Never write a partial object** from any of our own code paths in the meantime. See the |
| 110 | existing rule about this. |
| 111 | |
| 112 | ## How to check it worked |
| 113 | |
| 114 | 1. `SELECT LENGTH(option_value), autoload FROM wp_options WHERE option_name = 'mwai_options'` |
| 115 | before and after stage 1. Expect roughly 559 KB down to 35 KB on a comparable site. |
| 116 | 2. **The two-tab test**, which is the whole point: open Settings in two tabs, change the AI |
| 117 | environment in tab A and save, change a chatbot default in tab B and save, reload. Today |
| 118 | tab A's change is gone. After stage 2 both survive. After stage 3, tab B is told to |
| 119 | reload instead of being allowed to clobber. |
| 120 | 3. Same test with a persistent object cache enabled, since that is a common ingredient in |
| 121 | these reports. |
| 122 |