Thanks to visit codestin.com
Credit goes to github.com

Skip to content

🤖 feat(backup): back up and restore portable top-level config settings - #4229

Open
ibetitsmike wants to merge 9 commits into
mainfrom
mike/backup-settings-export
Open

🤖 feat(backup): back up and restore portable top-level config settings#4229
ibetitsmike wants to merge 9 commits into
mainfrom
mike/backup-settings-export

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Settings backup now carries the portable top-level config.json settings and restores them: per-agent model overrides (agentAiDefaults: model, thinking level, reasoning mode, enabled and advisor flags, sub-agent profile), default and hidden models, minimum thinking levels, model fallbacks, advisor settings, task settings, heartbeat and goal defaults, transcript width, debug-log toggle, runtime enablement and default runtime, and layout presets. Before this change the backup exported only a subset of userPreferences, so a restore on another machine left every one of these at the target's values.

Status

Head 12327c89. Paused for a scope decision (16:40Z). The automatic Codex pair on this head (rounds 11 and 12) completed: security clean; code review with two P2 findings. (1) Invalidated pre-write layout read: the generation guard keeps a stale getConfigForWrite response out of React state but still returns it, so a layout edit racing a restore is derived from the pre-restore presets; the read-modify-write window predates this PR, the guard made it observable. This is the fourth consecutive round with a new ordering finding in the layout mechanism (rounds 4/5, 7/8, 9/10, 11), so under the repository's non-convergence rule the next step is Mike's decision, put to him in the authoring chat: bounded fix forward (re-read when the pre-write fetch was invalidated), or take layoutPresets and the live layout refresh out of this PR, or keep as-is with a reasoned pushback. (2) Normalization-rejected values reset the target: a value the loose on-disk schema accepts but load-time normalization rejects (defaultModel: "garbage", a malformed hidden-models entry, an unknown runtime id, a layout slot the normalizer drops) was recorded as a null reset, deleting the target's valid setting instead of being reported as unsupported. Confirmed and fixed locally in 4d4f4b20 (fieldSchema tightened to what each normalizer accepts; empty spellings still mean unset; new test fails without it; 413 backup tests, typecheck, lint green), held unpushed so the branch takes one review pair after the decision. Since 27c72b9b this head carries the fixes for the three round-9 findings (runtime keys dirty-tracked at startup; one generation counter across every UILayoutsProvider request; last loaded presets kept when a later refresh fails), each with a regression test that fails without it. CI on this head: every job green except Codex Comments and Required, which fail while Codex threads are open. No remote UAT round has run on 12327c89; the delta since 27c72b9b (UAT round 5, passed) is renderer ordering and startup seeding covered by unit tests. Caveat alongside the deferred items: UAT round 5 showed that restoring defaultModel: null clears the key and a second origin of the same backend re-imports its stale mirror on reload (V3-01..V3-03); the UI has no way to clear the default model, so this state is newly reachable through restore, and the pre-existing import condition alone does not make it safe (see Deferred). Earlier provenance (Mike's direct merge request, rounds 7 to 10 beyond the six-review ceiling set for this PR, the Test / Unit QuickJS flake and its baseline) is condensed in the ledger toggle.

Background

Restoring a backup did not bring back the exec/plan model overrides configured under Settings > Agents, or any other top-level setting. createBackupPayload wrote AGENTS.md, agents, skills, global memory, redacted mcp.jsonc and a preferences.json projected from userPreferences alone; nothing read the rest of the config.

Implementation

  • src/node/services/backup/settingsProjection.ts: a fail-closed allowlist of the portable keys with the exclusion rationale (machine/network-local values, governor secrets, credential-derived routing, destructive archive policies, legacy save-time projections, internal state). Keys are validated one at a time against AppConfigOnDiskSchema (the on-disk schema is passthrough, so a picked object would carry any key of a repository-controlled document into the config).
  • The export is a complete snapshot: every listed key is present and null means unset, so a setting the user reset to its default (cleared agent overrides, deleted fallback chains, full-width off) round-trips as that default instead of a gap the restore fills from the target. agentAiDefaults spells its empty state as {}, the value config load normalizes to. A key that is absent from the block (an older build's export) keeps the local value.
  • One NORMALIZE table maps each key to the canonical in-memory value using the same functions Config applies on save and load (exported from src/node/config/index.ts for that purpose). Export, read, merge and the post-write verification share it, so a schema-valid but non-canonical document (a padded model string, an unsanitized fallback chain) compares equal to what config.json holds after the write instead of tripping the post-write IO_ERROR after files were written.
  • The block rides inside preferences.json under a sibling settings key. No new payload file and no schema bump: older builds refuse unknown payload paths while parsing the manifest, but their non-strict UserPreferencesSchema strips the unknown key, so they keep restoring what they understand.
  • Restore merges each present key over the local config inside the same editConfig edit as preferences (hiddenModels also marks migrations.hiddenModelsInitialized, as the UI path does); preview flags preferences.json as modified when settings differ.
  • Downgrade tolerance: a key whose value fails this build's schema (a thinking level or runtime added by a newer build, or a damaged document) keeps its local value instead of failing the whole restore. Preview and restore results name such keys in unsupportedSettings (same pattern as projectBundleSkipped) and the Backup section shows them, so the user knows what to reapply after updating.
  • Renderer: model, agent and runtime defaults are read from localStorage mirrors seeded from the backend at startup. A restore rewrites config behind them, which left Settings > Models and Runtimes stale and let the next hide/unhide persist the stale list over the restored one. Seeding now lives in seedConfigMirrors (authoritative: a value the backend no longer holds clears the mirror; startup dirty keys stay protected) and the Backup section re-seeds after a successful restore. UILayoutsProvider refreshes on onConfigChanged so restored layout presets apply without a reload.
  • Backup section's "Included" list gains "Model and agent settings".

Validation

  • Remote dogfood UAT through Coder Agents on two isolated Xum roots: round 1 on ece81ce2 verified export contents, exclusion of machine-local keys (including a grep of the whole backup repository for locally set marker values), preview/restore/re-preview, older-format and malformed backups, and found the stale-mirror clobber fixed in 116832b0; round 2 on 116832b0 passed (clobber fix confirmed, cross-tab Models and Runtimes live, machine-local keys smuggled into the block ignored, sanity). Round 3 on the final head aad7583a passed every scenario: reset round-trip with the 19-key snapshot and no archive keys in preferences.json, clobber and reload, Runtimes live, restored layout preset applied without a reload, unsupported-settings notice (a tampered thinkingLevel skipped and named after Preview and after Restore, restore not refused, the valid defaultModel next to it applied, local agent defaults untouched), and sanity on both roots. 27 unique screenshots plus the exported preferences.json and manifest. Round 4 on b8b2bc38 passed every scenario (35 unique screenshots): unknown layout preset version and non-object document reported as unsupported with the local slot untouched and still applying, notice cleared by save, preview and backup, unset advisor cap exported as 3 and an explicit unlimited cap as null with both applied on the target, layout refresh ordering under rapid restore-and-apply cycles, and the round-3 regression set. The critic noted that the notice stays while a Back up now attempt is in flight or fails; that is intended: a failed push does not replace the remote bundle, so the preview and the notice describing it remain accurate and are cleared together on success. Round 5 on the final head 27c72b9b (27 unique screenshots, adjudicated PASS for the targeted scenarios): V1 corrupt layout shapes ({"version":2,"slots":"corrupt"}, {"version":2,"slots":{"1":{"name":5}}}) reported as unsupported, restore completes, layoutPresets byte-identical before and after, slot 1 still applies; V2 a restore failing after its snapshot (project-memory file flipped read-only at the config rewrite by a timing watcher, a harness contrivance that was deterministic in three attempts) shows the snapshot path in the error, config already holds the restored default and hidden models, and the Models page reflects them live, keeps them when one more model is hidden, and after reload; V4 regression smoke (exec override reset live, 19-key block without archive keys, hidden-model clobber, layout version 3 unsupported, unset advisor cap exported as 3) passed; V5 sanity passed. V3 is an observation, not a pass: the manual control (clearing the default model in the UI) is not reachable because the UI has no way to clear it, and after restoring defaultModel: null at one origin a reload of a second origin (localhost versus 127.0.0.1, same backend) re-imported that origin's stale default model into config, which the first origin then showed after its reload; hiddenModels was unaffected. Cosmetic, pre-existing: the post-snapshot failure message joins two sentences without punctuation. Evidence under .mux-uat/round-5/ in the authoring workspace.
  • Unit and integration tests: projection inclusion/exclusion and null-means-unset, canonicalization of non-canonical input, wholesale merge semantics, unsupported values skipped and named through preview and restore, the older-build strip guarantee, restore through a real Config against a bare origin repository (including a source that reset its settings clearing the target's overrides), mirror re-seeding and clearing (including dirty runtime keys during the startup load), layout refresh ordering (out-of-order responses, a refresh completing after a save, a failed refresh after a successful load, the initial-load fallback).
  • tests/ui/BackupSection.test.ts has 7 failures on this branch that reproduce identically on the base commit 023701d; they are pre-existing and unrelated.

Risks

  • Restore semantics: a key present in the backup replaces the local value wholesale, including null resetting it to the default; only a key absent from the block leaves the target untouched. Restores now write into settings other services read at runtime (agent defaults, model fallbacks, task settings); every value passes the on-disk zod schema and the existing save/load normalization, and the safety snapshot taken before a restore also carries the settings block, so the undo path covers them.
  • Archive behaviors (worktreeArchiveBehavior, coderWorkspaceArchiveBehavior) are deliberately not portable: a repository-controlled "delete" would make later archives destructive with no prompt naming the policy.
  • WorkspaceContext startup seeding moved into the shared helper; the one behavior change is that a backend without a stored default model or hidden list now clears the corresponding mirror instead of leaving a stale value. Dirty-key protection now covers the runtime keys as well as the model keys, so a toggle made during the initial config load survives it.

Deferred (pre-existing, found during UAT)

Owner: Mike (this note is the follow-up record; issues to be filed on request). Trigger: after this PR merges, or earlier if one of them bites.

  • Checksum-mismatch error gives no remediation guidance.
  • No undo control for a restore in the UI (the safety snapshot path is reported in the status message).
  • Settings sections paint defaults before the config fetch resolves.
  • manifest.json sourceLabel is the exporting root's directory name.
  • Post-write verification window (thread). Disposition: the base already ran this exact check for userPreferences (edit, then a synchronous loadConfigOrDefault() read-back compared through the backup projection); this PR widens the compared set to the settings block, so more writers can in principle trip it. editConfig serializes every read-modify-write behind a single permit, so a concurrent edit is derived from the restored config and no restored value is lost; the read-back runs synchronously in the continuation of the same await, so a competing edit must complete a file write first, which makes the window practically unreachable. Consequence if hit: a spurious IO_ERROR naming the safety snapshot, after which the Backup section re-seeds the mirrors from the backend (27c72b9b). Fix: have editConfig expose its persisted result so the check binds to the edit.
  • Legacy model-preference re-import from another origin (thread). Disposition: migrateLocalModelPrefsToBackend is unchanged by this PR; it imports a non-empty localStorage defaultModel mirror whenever the backend has none, and a hidden-list mirror while hiddenModelsInitialized is false. What this PR adds is one more way to reach the "no default model" state: a restore carrying defaultModel: null. The restoring origin is re-seeded and does not re-import; the exposure is a second origin of the same config root (browser mode on another host or port, or Electron plus a browser) that was used while the model was set and loads again afterwards, which then re-imports its stale mirror. UAT round 5 reproduced exactly this (V3-01..V3-03) and found that the UI offers no way to clear the default model, so a cleared default is reachable only through restore; the pre-existing import condition therefore does not by itself establish that the newly reachable state is safe, and this stays a caveat on the feature until the import is gated (for example on a one-time migrations flag like hiddenModelsInitialized). A hiddenModels: null cannot come from a real source because load-time seeding always materializes an array, and the hidden-list import is gated on hiddenModelsInitialized. Fix: a persisted "model preferences initialized" marker that ends legacy re-import.
Review ledger (automatic code and security reviews count separately; rounds 7 to 12 were auto-triggered by pushes made on Mike's direct instructions and exceed the six-review ceiling originally set for this PR). CI note: `Test / Unit` on `27c72b9b` failed once in `WorkflowRunner > marks empty workflow returns as failed runs` with the QuickJS WebAssembly trap `Unreachable code should not be executed`, byte-identical to failures of unrelated tests in #3559 (run 27552085157) and #3695 (runs 32914518352, 32991170353); this diff touches no workflow or QuickJS file, the test passes locally, and the rerun passed.
# Review Head Outcome
1 Codex code review 116832b0 4 findings: resets, downgrade tolerance, layouts, canonicalization; all fixed in aad7583a
2 Codex security review 116832b0 1 advisory: archive policies; fixed by exclusion in aad7583a
3 Design advisor consultation (in the authoring chat) 116832b0 endorsed the fix design
4, 5 Codex code and security reviews aad7583a triggered by marking the draft ready; completed 05:02Z and 05:04Z. Code review: 3 findings, all confirmed: P1 newer layout preset version wipes local presets, P2 refresh race, P2 stale notice. Fixed in 00f1503d, pushed in b8b2bc38. Security review: no new findings. No approval on this head.
6 Fresh final advisor (clean-context sub-agent: goal, final diff, findings, tests, UAT evidence) aad7583a plus local 00f1503d BLOCKER: an unset advisorMaxUsesPerTurn exported as null, which the advisor reads as unlimited, so a restore from a default source switched the target to unlimited advisor uses; otherwise no blocker once 00f1503d is pushed. Fixed in b8b2bc38, pushed.
7, 8 Codex code and security reviews (automatic on the push; beyond the ceiling) b8b2bc38 completed 07:04Z. Security: no new findings. Code: 4 findings: layout shape fixed in 27c72b9b; verification window deferred (pre-existing); mirrors after partial failure fixed in 27c72b9b; legacy origin re-import deferred (pre-existing). No approval on this head.
9, 10 Codex code and security reviews (automatic on the push; beyond the ceiling) 27c72b9b completed 07:29Z and 07:23Z. Security: no new findings. Code: 3 findings, all confirmed, fixed in 12327c89: runtime mirrors at startup, mount refresh ordering, presets blanked on refresh failure. No approval on this head.
11, 12 Codex code and security reviews (automatic on the push) 12327c89 completed 16:24Z and 16:25Z. Security: no new findings. Code: 2 findings, both confirmed: invalidated pre-write layout read held for Mike's scope decision; normalization-rejected values reset the target fixed locally in 4d4f4b20, unpushed. No approval on this head.

Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: xhigh • Cost: $237.54

Settings backup exported only a portable subset of userPreferences, so
agentAiDefaults, defaultModel, hiddenModels, advisor, task, heartbeat, goal,
archive-behavior, runtime and layout settings were silently left out of export
and restore.

The new settingsProjection module owns a fail-closed allowlist of those
top-level config.json keys. They ride inside preferences.json under a sibling
`settings` key instead of a new payload file, so older builds (whose manifest
parser refuses unknown paths but whose UserPreferencesSchema strips unknown
keys) keep restoring the preferences they understand. Restore merges the block
in the same config edit as the preferences, replaces carried keys wholesale,
leaves machine-local keys alone, and extends the post-write verification.
Behavior-neutral cleanup of the portable settings backup:

- settingsProjection.ts: shorten the save-path mirroring comment to three
  lines and reduce the readBackupSettings doc to its rationale.
- settingsProjection.test.ts: build the projection expectation from one
  portable fixture instead of repeating it, and assert the merged task
  settings exactly rather than with a greater-than-zero check.
- payload.test.ts: spread a preferences fixture instead of a rest
  destructure with a throwaway binding.

Each tightened test was proven to fail with the guarded behavior removed.
…d settings fields

A restore rewrites defaultModel, hiddenModels, agentAiDefaults, runtimeEnablement and defaultRuntime in config while the renderer keeps reading its startup-seeded localStorage mirrors, so the Models and Runtimes pages stayed stale and the next hide/unhide persisted the stale list over the restored one. The startup seeding moves into a shared helper that the Backup section reuses after a successful restore. A malformed settings block now reports the offending fields instead of the raw issue list.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T16:24:35.269256Z 12327c8 New commits
🔒 Security Review Completed 2026-09-13T16:25:46.622489Z 12327c8 New commits

Security findings

Advisory findings (1)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 116832b0d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/backup/settingsProjection.ts Outdated
Comment thread src/node/services/backup/settingsProjection.ts Outdated
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx Outdated
Comment thread src/node/services/backup/settingsProjection.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 116832b0d4

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/node/services/backup/settingsProjection.ts Outdated
…d values

The settings block is now a complete snapshot: every portable key is present, null meaning unset, so a setting the user reset to its default (cleared agent overrides, deleted fallback chains, full-width off) round-trips as that default instead of a gap the restore filled from the target. Export, read, merge and the post-write check share one normalizer table built from the same functions Config applies on save and load, so a schema-valid but non-canonical document no longer trips the post-write IO_ERROR after files were written. The renderer mirror re-seed is authoritative (a value the backend no longer holds clears the mirror) and UILayoutsProvider refreshes on config changes so restored layout presets take effect without a reload.
A restore no longer refuses the whole backup when the settings block holds a value this build's schema does not know (a thinking level or runtime added after it shipped, or a damaged document). Each such key keeps its local value and is named in the preview and restore results, which the Backup section shows, so a downgrade still restores everything else and the user knows what to reapply after updating.
@ibetitsmike
ibetitsmike marked this pull request as ready for review September 13, 2026 04:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aad7583a0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/backup/settingsProjection.ts
Comment thread src/browser/contexts/UILayoutsContext.tsx Outdated
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx
… clear stale notices

The on-disk schema leaves layoutPresets unknown, so a backup written by a build with a newer preset version passed the per-key check and its normalizer read the unknown version as empty, deleting the target's presets. The versions this build migrates are now pinned, so such a document is reported as unsupported and the local presets stay. UILayoutsProvider awaits each refresh in its config subscription so two quick changes cannot land out of order, and the Backup section clears the unsupported-settings notice wherever it invalidates the preview it came from.
… unlimited

The advisor reads advisorMaxUsesPerTurn === null as "unlimited" and undefined as the default cap, while the settings block spells unset as null. A source at the default therefore exported null, and every restore switched the target's advisor to unlimited uses per turn. The export now writes the default cap for an unset value and keeps null only for a genuinely unlimited source.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8b2bc380d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/backup/settingsProjection.ts Outdated
Comment thread src/node/services/backup/adapters.ts
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx Outdated
Comment thread src/browser/utils/configMirrors.ts
…ors after a failed restore

A document such as {"version": 2, "slots": "corrupt"} passed the version-only check, after which the normalizer read the slots as empty and the restore deleted the target's presets. The backup field now uses the strict schema the layouts API saves through, so any preset document this build cannot parse is reported as unsupported. The Backup section also re-seeds the renderer mirrors when a restore fails after its safety snapshot, since config may already have been rewritten by then.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27c72b9bae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/utils/configMirrors.ts Outdated
Comment thread src/browser/contexts/UILayoutsContext.tsx
Comment thread src/browser/contexts/UILayoutsContext.tsx
… config responses

Codex round 9 on 27c72b9, three P2 findings in code this PR added:

- The startup mirror seed wrote the runtime mirrors unconditionally, so a runtime toggle made
  while the initial getConfig() was pending was reverted by the stale response. The runtime keys
  are now tracked as dirty like the model keys and seedConfigMirrors honors skipKeys for them;
  the post-restore re-seed still clears authoritatively.
- UILayoutsProvider ordered only subscription refreshes; the mount refresh, pre-write fetches
  and saves could still land last with older presets. One generation counter now covers every
  request that installs presets: only the newest one applies its result.
- A transient getAll() failure on a subscription refresh replaced the loaded presets with the
  empty default. Post-load failures now keep the last loaded presets and only flag loadFailed.

Tests: runtime toggle during a pending startup config load (and backend authority afterwards),
out-of-order refresh responses, a refresh that started before a save, a failed refresh after a
successful load, and the initial-load fallback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12327c891c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

//
// This prevents stale in-memory state (captured by closures) from accidentally overwriting a
// newer config when multiple writes happen in sequence (e.g., delete layout → clear hotkey).
const generation = ++generationRef.current;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry invalidated pre-write layout reads

When a layout edit's getConfigForWrite() request captures old presets and a concurrent restore/config-change refresh advances the generation and installs newer presets, this guard prevents the old response from updating React state but the function still returns that stale normalized value. The caller then derives its edit from the old layout and saveAll() writes it over the restored presets; retry or discard invalidated pre-write reads instead of returning them.

AGENTS.md reference: AGENTS.md:L167-L167

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as described: the generation guard keeps an invalidated pre-write read out of React state, but getConfigForWrite still returns it, so a layout edit racing a restore derives its edit from the pre-restore presets and saveAll writes them back. For the record: the read-modify-write window itself predates this PR (getConfigForWrite followed by saveAll was never serialized against an external config rewrite); what is new is that the guard makes the invalidation observable. This is the fourth consecutive review round with a new ordering finding in the layout mechanism (rounds 4/5, 7/8, 9/10, 11), so per the repository's non-convergence rule the next step is a scope decision by Mike rather than another fix: either re-read when the pre-write fetch was invalidated (bounded retry), or take layout presets and the live layout refresh out of this PR. Held open until that decision.

Xum replied on Mike's behalf.

Comment on lines +186 to +188
const parsed = fieldSchema(key).safeParse(block[key]);
if (parsed.success) {
setSetting(settings, key, NORMALIZE[key](parsed.data));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve local settings when normalization rejects a value

When a backup contains a scalar that passes the loose on-disk schema but its normalizer rejects—for example defaultModel: "garbage"safeParse succeeds here and setSetting converts the normalizer's undefined result into the explicit null reset. Restoring that damaged document therefore deletes the target's valid default model instead of retaining it and reporting the field as unsupported; distinguish valid canonicalizable values from rejected ones before recording the setting.

AGENTS.md reference: AGENTS.md:L127-L127

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. normalizeOptionalModelString("garbage") returns undefined, setSetting recorded that as null, and the merge deleted the target's default model; the same path existed for a hidden-models list with a malformed entry, a fallbacks object whose every chain the sanitizer drops, an unknown runtime id in runtimeEnablement, and a schema-valid layout slot the normalizer drops. Fixed locally in 4d4f4b20 (validated: 413 backup tests, typecheck, lint; a new test fails without the fix): fieldSchema tightens those keys to what their normalizer accepts, so the values surface in unsupported and the local value is kept, while empty spellings ("", [], {}, no slots) still canonicalize to unset. Not pushed yet: it goes out together with the outcome of the scope decision on the sibling layout thread, so the PR takes one more review pair rather than two.

Xum replied on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Review loop paused for a scope decision (repository non-convergence rule): the Codex round-11 code review on 12327c89 raised a fourth consecutive ordering finding in the layout-preset mechanism (thread). Its sibling finding on the settings projection (thread) is confirmed and fixed locally in 4d4f4b20, held back so the branch takes one review pair after the decision. Options put to Mike in the authoring chat: (A) bounded fix forward in getConfigForWrite, (B) drop layoutPresets and the live layout refresh from this PR, (C) keep as-is with a reasoned pushback. Security review on 12327c89 completed with no new findings; every CI job on this head is green except the Codex gate.

Xum commented on Mike's behalf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant