Skip to content

🤖 feat: back up Mux settings to a git repository - #3767

Open
ibetitsmike wants to merge 202 commits into
mainfrom
mike/backup-sync-dbgh
Open

🤖 feat: back up Mux settings to a git repository#3767
ibetitsmike wants to merge 202 commits into
mainfrom
mike/backup-sync-dbgh

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds phase 1 of settings backup: a Backup settings tab that pushes a portable subset of ~/.mux to a git repository the user specifies, and restores it on another deployment. Backup and restore are manual, the repository is addressed by URL rather than a new sign-in flow, and MCP commands and URLs remain usable on a fresh device while literal header values stay redacted.

Background

Mux user state lives entirely under MUX_ROOT on one host, so a second deployment or a fresh install starts from scratch. The obvious idea, "sign in with GitHub and use the existing login", does not work today:

Surface Scope What it retains
Server login device flow (serverAuthService.ts:12-18) read:user Discards the GitHub token; keeps only a hashed Mux session token
Copilot device flow (copilotOauthService.ts:11-19) read:user Stores the token as github-copilot.apiKey
GH_TOKEN forwarding (credentialForwarding.ts:28-30) n/a Forwarded into Docker/Devcontainer only

Neither token can clone or push, and there is no GitHub App or repo scope anywhere in the repo. Storing an OAuth token or PAT for backups, even a read-only one, is out of the question, so this PR adds no token input, no token storage, and no token pathway of any kind. Mux never holds token material itself: authentication is delegated entirely to credentials that already live on the backend host.

Implementation

Credential ladder (credentials.ts). One controlled attempt, then an ambient retry. The controlled attempt clears inherited helpers (-c credential.helper=) so the mechanism that authenticated is known and reportable in the UI:

  1. SSH URL: ambient agent/keys, with -o BatchMode=yes appended to any ambient GIT_SSH_COMMAND so a passphrase prompt fails instead of hanging, without discarding a host's ssh wrapper.
  2. HTTPS where gh auth status --hostname <host> succeeds: -c credential.helper='!gh auth git-credential'. This is the honest way to reuse an existing gh login, and the token never passes through Mux. GH_TOKEN, GITHUB_TOKEN, and their enterprise variants are stripped from both the probe and the helper invocation, since gh would otherwise consume them ahead of its stored login and quietly turn this rung back into a token pathway.
  3. Auth-class failure falls back to whatever ambient helper exists, still non-interactive.

Every rung sets GIT_TERMINAL_PROMPT=0, GH_PROMPT_DISABLED=1, GCM_INTERACTIVE=never. Repository settings reject credentials in URL userinfo and high-confidence query or fragment parameters before the URL can reach config.json or cache git metadata.

Dedicated secret stores have no export path. This is enforced structurally, not by filtering: payload.ts walks an explicit path allowlist, so providers.jsonc, secrets.json, mcp-oauth.json, server.lock, and serverAuthSessions.json have no code path into the payload. A closed-world test seeds all of them into a fake MUX_ROOT and asserts none appear in the pushed tree. Free-form documentation still passes through the high-confidence scanner. Every exported MCP command requires exact-payload approval, and MCP URLs receive structural checks for raw, malformed special-scheme, and WHATWG-normalized authority userinfo plus credential-like query or fragment parameters, including low-entropy values. Any override is bound to the exact payload bytes the user reviewed.

What syncs: AGENTS.md, agents/*.md, skills/** (execute bits preserved, so skill scripts stay runnable), memory/global/**, mcp.jsonc (commands and URLs verbatim, portable runtime fields, header names, and secret references; literal header values redacted), and preferences.json. Preferences use an explicit versioned allowlist because AppConfigOnDiskSchema is .passthrough(), so a denylist would leak every future key; the same reasoning excludes the record-typed ai.providerOptions providers, whose schema is z.record(z.unknown()). Project-path-keyed and workspace-keyed entries are excluded as machine-local, and AGENTS.local.md on the strength of its own .local marker. appearance.editorConfig is excluded too: its customCommand reaches spawn(..., { shell: true }), so a restore from a repository someone else can write to would be a code-execution vector. Symlinks are never collected, and neither is any hidden file: one rule applied to every path segment keeps .git object stores, .env and its variants, and .netrc out of the recursive collections, where the secret scanner would not recognise a low-entropy value like PASSWORD=hunter2 anyway.

MCP commands and URLs are portable data. Bare stdio command strings and object command and url values are stored verbatim so a fresh device can reconstruct its servers. Every exported command, recognizable secret, and structurally credential-bearing URL component blocks push until the user approves the exact payload digest. Restore separately derives every runnable command it would introduce, change, or enable from the exact bytes it would write and refuses with COMMAND_APPROVAL_REQUIRED until the user approves that text. A command shadowed by a non-empty URL needs no restore approval because McpConfigService.normalizeEntry() ignores it.

Redaction metadata disambiguates literal marker values. New manifests persist exact JSON paths for values the exporter redacted, including an explicit empty list when mcp.jsonc has no redactions. Restore treats only those paths as placeholders, so an approved command whose literal text is __MUX_BACKUP_REDACTED__ remains executable data and still goes through command approval. Metadata is validated against the final serialized MCP document, duplicate manifest and JSONC keys are rejected, and projection-dropped keys cannot leave stale paths behind.

Unrecognized MCP fields remain local. McpConfigService.normalizeEntry() reads transport, disabled, toolAllowlist, url, command, and headers, and ignores the rest, so a field like env: { "API_KEY": "hunter2" } is a credential nobody would notice leaving. Export projects exactly the fields Mux reads, at the types it reads them as; every other value becomes a marker and restore uses only the local value at that exact path. Portable-field lookups require an own property, so document-controlled names such as toString and constructor cannot resolve inherited validators.

Header credentials never come from the repository. Export replaces literal header values with a redaction marker. A portable { secret: NAME } reference may remain in the repository projection, but restore keeps a header only when the local file already has a value at that exact path and the restored entry still points at the same endpoint. A changed endpoint or a fresh device with no local header value gets no credential, so a backup cannot redirect a locally resolved secret.

Repository exports strip MCP comments, while restore preserves local-only definitions. A comment is prose the projection cannot inspect, so export reserializes the projected values instead of publishing the user's text. Restore applies edits in place, keeps destination-only server definitions, and carries their raw JSONC comments and trivia with them. Same-name definitions from the backup remain authoritative.

Older marker-based backups remain safe. A manifest without redaction-path metadata is treated as a legacy backup, so marker-bearing values still rehydrate from local state when available. If no usable local value exists, restore removes the executable marker or unusable server instead of passing marker text to the runtime. Existing repositories remain readable without a schema bump, while their next write adds exact metadata and prevents marker ambiguity.

Only documentation publishes from a recursive collection without review. skills/** and memory/global/** hold whatever the user put there, and no content scanner can decide whether an arbitrary file is a credential, since {"password":"hunter2"} has no distinguishing shape. So the gate is structural: anything that is not .md, .mdx, .markdown, or .txt, plus any file whose path promises credentials or has the exact conventional stem auth or passwd, is held back until the user approves that exact payload. The approval is a hash of every flagged path and its contents, so approving one payload cannot publish a different one another window wrote meanwhile.

Everything lands in one managed subdirectory (default mux/) so an existing dotfiles repo is a safe target, and a persistent cache clone under <MUX_ROOT>/backup-cache/ makes the two-direction preview cheap. Both sides check sizes before reading, since a repository is written by anyone who can push to it and a payload is buffered whole. The clone is sparse, single-branch, and blob-filtered, so a dotfiles repo with substantial history elsewhere is neither materialized nor transferred, and a path this platform cannot create never blocks a backup. Local paths and file: URLs use an upload-pack transport with filtering enabled for both clone and later fetches, preventing Git's local optimization from copying the full object database. Cache creation preserves the remote's SHA-1 or SHA-256 object format when the backup branch is missing, including zero-ref remotes, and SHA-256 clones retain their required extension during config rebuilding. Cache corruption cannot be enumerated, since an empty HEAD, a truncated index, and a ref naming nothing all keep the shape a structural check looks for and only fail a later Git command. So any unrecognized failure discards the disposable cache and retries once rather than leaving Preview, Push, and Restore blocked. Refusals to touch content the cache cannot prove it owns are excluded from that rebuild, as are outages and origin mismatches, so neither a foreign path nor a healthy cache is deleted; the discard itself rechecks ownership at the point of deletion. Relative local repository paths are anchored to the stable parent of <MUX_ROOT>/backup-cache (MUX_ROOT itself), so terminal and desktop launches use the same repository and git -C cannot reinterpret the path from the cache. Before Git runs, Mux rejects symlinks and special files under .git and replaces multiply-linked metadata with cache-owned inodes, so Git cannot rewrite an outside hard-link alias; new local clones also disable Git's hard-link optimization. The cache config is rebuilt from validated platform flags and known forced values on every use, so malformed retained settings cannot brick later retries. The payload is treated as bytes: end-of-line conversion is pinned off in the cache, because the manifest checksums what was written and a restore writes what it reads.

Local payload work is serialized across repositories. The repository lock is keyed per repository and branch, but every payload adapter reads and writes the one Mux root, so a second lock on a single key wraps the local payload work and is always taken inside the repository lock. Without it a push could export a half-restored root and publish it as a complete backup. Git and network work stays outside it, so different repositories still run in parallel. Serialization is process-local; cross-process locking is out of scope.

Validation

Beyond unit tests, a service-level integration test drives a real bare repository end to end and asserts the security invariant on bytes that actually reached a remote. Dogfooded all 12 planned scenarios against a live dev server and a real bare repo, which found 8 defects that are fixed here:

Defects found and fixed during validation
  1. BackupService declared its two dependency interfaces but nothing implemented them, so the service container did not compile.
  2. Every export rewrote manifest.exportedAt, so an unchanged backup produced a commit that only churned metadata.
  3. Restore preview always reported preferences.json as added, because preferences live in config and have no local file to compare against.
  4. createBackupPayload threw on detected secrets before the service could apply the user's override, making the override unreachable.
  5. isPortableReference treated $VAR / ${VAR} / env:VAR header strings as portable and exported them verbatim, but MCPHeaderValue is string | { secret }, so Mux never interpolates those and they are literal credentials.
  6. Restore reserialized mcp.jsonc through JSON.stringify, discarding the document the user had just previewed. Restore now applies jsonc edits in place. (The export deliberately does reserialize, so that no comment ships.)
  7. Preview against a repository with no backup yet leaked a raw ENOENT containing the internal cache path.
  8. The secret-scan override checkbox only rendered inside the preview panel, so a blocked push had no visible way to proceed.

Each fix has regression coverage. The current MCP portability work covers verbatim commands and URLs, literal-header redaction, exact-payload publish approval for every command, exact runnable-command restore approval, literal redaction-marker commands, strict persisted metadata validation, metadata-less legacy backups, prototype-colliding portable-field names, projection-dropped __proto__ keys, disabled URL-to-stdio transitions, local-only server preservation, malformed map rejection, JSONC comment retention, low-entropy and malformed special-scheme URL credential gating, conventional credential filename stems, special-file-safe local reads, and a real bare-repository push. Repository URL tests cover credential-bearing userinfo, query, and fragment forms while retaining benign parameters. make static-check passes, and the configured Storybook story was verified at 1200 px and 390 px with no horizontal overflow.

Risks

Restore mutates user config, but it is deliberately not a mirror: it overwrites paths present in the backup, preserves local-only files and MCP servers, and writes a safety snapshot to <MUX_ROOT>/backup-cache/restore-<id>/ first. Restore planning rejects existing directories and special files before any destination is opened for writing. Optional local MCP reads preflight special files and use a nonblocking checked open so a path replacement race fails instead of hanging restore. Commands and URLs are repository-controlled input, so every exported MCP command and credential-bearing URL component requires exact-payload approval before publication, and changed runnable commands require separate exact-text approval before restore. Literal header values are never exported, and a local header is restored only to its original endpoint. Pushes use --force-with-lease against the commit the preview read, so a remote that moved is reported as drift rather than overwritten, and a server-side denial such as a protected branch is reported as itself.

Blast radius outside the feature is small: the shared-file edits are the settings tab registration, an added settingsBackup config key, and the service container wiring.

Unit-test infrastructure (now on main)

CI's Test / Unit job began failing repo-wide when runner-side test file ordering shifted and exposed latent cross-file state leaks in bun test. Those fixes landed on main via #3817 (including the SshPromptDialog mock restore and inline TaskGroupListItem menu stub this branch pioneered), so after rebasing onto that merge this PR no longer carries any shared test-infrastructure changes.

Out of scope

Continuous/automatic sync, drift indicators, three-way merge, plans and project-scoped memory, and mirror-mode restore. A "sign in with GitHub" flow is rejected, not deferred: it would require Mux to hold an OAuth token, and storing one, even read-only, is out of the question.


Mux opened this PR on Mike's behalf.


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 4b7a54c1d0

ℹ️ 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/payload.ts Outdated
Comment thread src/node/services/backup/gitRepo.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Thanks, three of the four were real. Fixed with red-green verified regression tests (removed each guard, confirmed the intended test fails, restored).

1. Windows separators in manifest paths (payload.ts:101) — fixed. Correct: skills/..\..\escaped.md passed the allowlist because isAllowedPayloadPath's /^skills\/.+/ matches it and the .. check only split on /. Harmless on Linux, where a backslash is an ordinary filename character, but path.join treats it as a separator on Windows and escapes the destination. assertAllowedPayloadPath now rejects any payload path containing a backslash, since payload paths are always posix. Test: "rejects payload paths that escape the destination on Windows".

2. Managed paths deleting the repository cache (gitRepo.ts:50) — fixed. Correct, and the reachable trigger is worse than traversal: the managed path is the user-facing "Subdirectory" field, so . was accepted and would widen git clean -fd -- . and git commit -- . to the entire cache clone. assertSafeRelativePath now rejects . and empty segments alongside .., and I added the same rule at the schema boundary (isValidBackupPath) so the UI rejects it before any git command runs. Test: "refuses managed paths that are not a real subdirectory", covering ., ./, .., mux/../.., /mux, and mux\..\...

3. Bare key query parameters (payload.ts:263) — fixed. Correct on both halves: api[_-]?key required the api prefix so a Google-style ?key=AIza... was not redacted, and the fallback scanner had no AIza pattern, so it would have been pushed without even triggering the override. Added isSensitiveParamName (matches a bare key as a whole component) and an AIza pattern to SECRET_PATTERNS. Two tests: URL redaction, and free-form file detection.

4. Native titles (BackupSection.tsx:436) — respectfully disagree, no change. These are not native DOM tooltips. Every title in this file is on the shared Button, which deliberately intercepts the prop: Button.tsx:37 does Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "title">, :40 redeclares title?: React.ReactNode, :64 sets resolvedTooltip = tooltip ?? title, and :95 renders <TooltipIfPresent tooltip={resolvedTooltip}>. No native title attribute reaches the DOM, so there is no OS tooltip to duplicate or z-index. Button.tsx:61-63 documents this as intentional so callers can keep using the prop they reach for. The remaining title= occurrences in the file are props on ChangeList (a section heading) and ConfirmationModal, neither of which is a DOM attribute either. Happy to switch to an explicit tooltip prop if you'd prefer that for clarity, but the AGENTS.md concern about duplicate OS tooltips does not apply here.

Mux made these changes on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Please take another look.

@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: e9a25a8260

ℹ️ 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/common/orpc/schemas/backup.ts Outdated
Comment thread src/node/services/backup/adapters.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/adapters.ts Outdated
Comment thread src/node/services/backup/gitRepo.ts
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

All five addressed, each with a red-green verified regression test (removed the guard, confirmed the intended test fails, restored).

1. .git components in backup paths — fixed. Confirmed reachable: the managed path is the user-facing "Subdirectory" field, so .git or .git/hooks would have written into the cache clone's own git directory. Rejected in three places now: isValidBackupPath (schema refine), assertSafeRelativePath (the git guard), and BackupService.saveSettings, which owns the invariant rather than trusting the transport. Test: "rejects a managed path that targets the git directory".

2 & 3. Symlink traversal on read and restore — fixed. I confirmed the premise before fixing: git stores symlinks as mode 120000, so a backup repository can carry one, and fs.readFile follows it.

$ git ls-tree HEAD mux/AGENTS.md
120000 blob 48980ad58db1b502c17dd015c92dd262ee8092af   mux/AGENTS.md

Added one shared helper, resolveContainedPath(root, relativePath), which walks the path segment by segment and rejects any component that lstat reports as a symlink. Applied at all three sites rather than only the two reported: the payload write, the payload read, and the restore write. Two tests: reading an entry that was replaced with a symlink to a file outside the payload, and restoring into a MUX_ROOT whose skills directory is a symlink (the test also asserts the outside directory stays empty).

4. Validate payload contents before writing — fixed. readBackupPayload now parses preferences.json and mcp.jsonc after verifying hashes, so a corrupt payload is rejected there. Since restore calls validateRestore before taking the snapshot and before any write, a malformed backup can no longer leave a partially restored install. That also removes the case you noted where the error omitted the snapshot path: no snapshot exists yet when validation fails. Test asserts the target directory is still empty after rejection.

5. Non-fast-forward mapped to drift — fixed. Correct, and the cause was exactly as described: BackupNonFastForwardError had no code, so toOperationError fell through to IO_ERROR. Both error classes now carry one (REPOSITORY_CHANGED and GIT_ERROR for BackupOriginMismatchError, which had the same defect).

One note on how I tested it, since it changes the claim: I first tried to force the drift through the real adapter by moving the remote between pushes, and the push succeeded. That is not the bug you found, it is by design: prepare() fetches and resets before each operation, so a remote that moved is simply the new base and the push fast-forwards from it. The git-level non-fast-forward is therefore a narrow race between prepare and push, which I could not force deterministically. I deleted that test rather than keep a misleading one, and instead assert the mapping you actually flagged by rejecting the real BackupNonFastForwardError from commitAndPush and checking the resulting code. Warning the user when the remote moved since they last read it is a separate feature, tracked for a later phase.

Mux made these changes on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Please take another look.

@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: e9a25a8260

ℹ️ 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/serviceContainer.ts Outdated
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

CI's Test / Unit failure was a real defect in this PR, not flake, and it is fixed.

Root cause. src/browser/hooks/useAnalytics.test.tsx:135 mocks @/version as a bare string:

void mock.module("@/version", () => ({ VERSION: "test-version" }));

CI runs all unit files in one Bun process, so that module mock reaches this feature. VERSION.git_describe was then undefined, JSON.stringify dropped muxVersion from the written manifest, and parseManifest rejected our own output on the next read. With no readable previous manifest there was nothing to reuse, so every export wrote a fresh exportedAt and each backup looked changed. That is why exactly the six no-op and round-trip tests failed while the rest passed.

This mattered beyond the test: any build whose version metadata is unavailable would have written a manifest that this code cannot read, i.e. an unrestorable backup.

Fix. muxVersion is normalized when the manifest is built, so the field is always present, and it is resolved defensively at the boundary in adapters.ts. Manifest validation stays strict on read. Two tests pass muxVersion: undefined directly rather than depending on mock ordering, so they reproduce the condition deterministically.

One correction to my own work. My first attempt also relaxed parseManifest to tolerate a missing muxVersion. Toggling showed normalization alone is sufficient, so the relaxed reader was weakening validation for no benefit and I reverted it. With the strict check restored, removing the normalization fails both new tests, which is the attribution I wanted before claiming the fix.

Local validation: 48 backup tests, 5 UI tests, make static-check exit 0.

Mux made these changes on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Please take another look.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Both correct, and both were consequences of my own earlier fixes. Fixed.

1. Configured token never reached git (serviceContainer.ts:161). This was the more serious of the two: createBackupGitRepo accepted only { cacheRoot } and never set token, so rung 3 of the credential ladder was unreachable in production even though the tab tells the user Mux will try "configured tokens". credentials.ts handled the token correctly; nothing ever supplied one. The container now passes getToken: () => resolveGhToken(), reusing the existing helper at credentialForwarding.ts:28-30 so this matches how the rest of Mux resolves GH_TOKEN. It is resolved per call rather than captured at construction, so a token added after startup works without a restart. Test asserts the getter is consulted on a real operation.

2. Override shortcut still gated on preview (BackupSection.tsx:358). Correct, and it was a gap I introduced: when I made the checkbox appear on a blocked push, I updated the render condition but not the shortcut handler, so the advertised keybind was inert in exactly the situation the checkbox exists for. The handler now mirrors the render condition (preview || secretScanBlocked). Extended the existing "exposes the override after a secret-scan block" test to drive the keybind and assert the checkbox actually toggles, rather than only asserting it renders.

Local validation: 49 backup tests, 5 UI tests, make static-check exit 0.

Note on the Codex Comments job failure on the previous head: that job starts seconds after a push and fails if any thread is unresolved at that moment, so it raced the new review rather than reporting a code problem. Threads for this round are resolved before this push.

Mux made these changes on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Please take another look.

@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: 5221090a2e

ℹ️ 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/payload.ts
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/adapters.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Addressed all four findings from the previous round in 8b6b900 and 9700219, each with a red-green verified regression test:

  • Managed-root ancestors are resolved through resolveContainedPath() (per-segment lstat) before any read or recursive removal.
  • Stdio MCP commands are redacted in both supported shapes (object command and the shorthand string entry), covering --flag value, NAME=value, and Bearer <token>, while $VAR is preserved because the command runs through bash -c.
  • The execute bit now travels in the manifest and is reapplied on export and restore, the way chmod +x does it.
  • Restore preview compares against the rehydrated content restore would write, so a redacted value with a surviving local value no longer reports as modified.

Replies are on each thread and the threads are resolved.

Mux is working on this PR on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

One more preview correction pushed in fb41dd9: restore merges preferences into the local config rather than replacing a file, so preview now compares the merge result. A backup whose preference values all match locally no longer reports preferences.json as modified. Red-green verified against the previous byte comparison.

Mux is working on this PR on Mike's behalf.

@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: fb41dd907c

ℹ️ 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/adapters.ts Outdated
Comment thread src/node/services/backup/gitRepo.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Two more fixes from a self-review pass, pushed in 8babbc6 and 0050d1b. Both are red-green verified.

Restore was dropping machine-local preferences (data loss). restore() handed restoreBackupPayload the exportable projection as the local side, and the merge result then replaced the whole stored preference object. Keys the backup deliberately excludes (navigation.projectOrder, review.defaultBaseByProject) were deleted on restore. Reproduced with a probe, fixed by passing the full local preferences and keeping the projection for export and diffing only.

Redaction and mode gaps. Bare PASSWORD=value assignments and quoted values (--api-key "two word secret") now redact as one token; '$VAR' is no longer treated as a shell reference, because single quotes suppress expansion. Top-level files go through the same collection helper as directories so every entry carries its mode, restore clears the execute bit when the backup says non-executable, and preview and changedFiles compare the bit.

One deliberate non-change, so it is on the record: a redaction marker makes the whole JSONC scalar locally owned, so a non-secret edit the backup made inside that same string is not restored. This is intentional. Splicing a local credential into backup-controlled text would let a tampered backup move that credential to a different host or binary. The policy is stated at the restore rule and pinned by a test.

@codex review

Mux is working on this PR on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round addressed in c00444e (plus 8babbc6 and 0050d1b, pushed just before this review landed):

  • Merge into unprojected local preferences (P1): confirmed and fixed. I hit the same defect in a self-review and reproduced it with a probe. Restore now merges into the full local preferences; the projection is only for export and diffing.
  • Force-stage payload files (P1): confirmed and fixed. Staging is now git add -A -f. A seeded .gitignore test asserts mux/preferences.json reaches the pushed tree.
  • Clear executable bits (P2): confirmed and fixed. Restore applies the bit in both directions, and preview plus changedFiles compare it.
  • Rehydrate only the redacted portion (P2): deliberately not changed, argued on the thread. A marker makes the whole scalar locally owned because partial substitution would let backup-controlled text choose where the local credential is sent (vendor.example to attacker.example). Preview and restore share one helper so they agree, the affected fields are surfaced in redactions, and the policy is pinned by a test.

Every fix is red-green verified. Backup suite is 57 tests, make static-check is green.

Mux is working on this PR on Mike's behalf.

@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: c00444e125

ℹ️ 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/gitRepo.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Both findings confirmed and fixed in 6e60656, each red-green verified:

  • Remove ignored payloads when refreshing the cache (P1): cleanManagedPath now uses git clean -fdx. A payload an earlier preview or blocked push left behind as an ignored file can no longer survive the reset and be read back as remote state. Test seeds .gitignore with mux/, exports without pushing, re-prepares, and asserts preview sees no remote backup.
  • Preflight every restore destination (P2): restoreBackupPayload resolves all destinations and restored content before the first write. The symlink test now seeds a local AGENTS.md (sorts before skills/) and asserts it is untouched after the symlinked destination is rejected.

Backup suite is 58 tests, make static-check green.

Mux is working on this PR on Mike's behalf.

@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: 6e60656f6b

ℹ️ 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/serviceContainer.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/adapters.ts
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All four findings confirmed and fixed in e667e5c, each red-green verified:

  • Limit GH_TOKEN to GitHub hosts (P1): the factory now passes the token only when the backup URL is a GitHub host (github.com, a *.github.com subdomain, or GH_HOST), rejecting lookalikes like github.com.evil.example. Separately, the token helper is now scoped to the repository's own origin instead of the global credential.helper, so a redirect or submodule on another host cannot be offered the token either.
  • Redact credentials inside stdio command URLs (P1): command redaction now runs the existing URL rules over any http(s):// argument first, so npx mcp-remote https://host/mcp?api_key=... is covered.
  • Preserve MCP secrets in local safety snapshots (P1): the snapshot is written with keepLocalSecrets: true. It never leaves the machine, so keeping mcp.jsonc verbatim is what makes it able to recover a server the restore removed outright.
  • Reject incompatible restore targets (P2): resolveContainedPath now rejects a non-directory intermediate component, and the restore preflight rejects a destination that is an existing directory. Both happen before the first write.

Backup suite is 60 tests, make static-check green.

Separately, the earlier Test / Integration failure was tests/ui/review/focus.test.ts ("Review panel not focused"), which is unrelated to this branch: my only keybinds are ctrl+alt+<letter>, none collide with Cmd/Ctrl+2, and that test passes locally. I reran the job.

Mux is working on this PR on Mike's behalf.

@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: e667e5c173

ℹ️ 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/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All four confirmed and fixed in 8382ddb, each red-green verified:

  • Reject symlinked top-level backup files (P1): collection is now uniformly no-symlink. collectDirectory already skipped symlinked entries, but top-level files used stat and a symlinked collection root was followed by readdir. Both fixed, which matches the restore side that already refuses to write through symlinks.
  • Exclude nested Git metadata (P1): .git entries are skipped during collection and .git components are rejected in assertAllowedPayloadPath, so a manifest cannot smuggle one in either.
  • Project provider options through a closed allowlist (P1): this was my own rule broken in one spot. Only providers with a closed z.object schema (anthropic, openai, xai) are copied; the record-typed google/ollama/openrouter are excluded, and a provider added later is excluded by default. The guard iterates MuxProviderOptionsSchema.shape and injects apiKey under every provider, so adding a record-typed provider to the allowlist fails the test.
  • Redact non-Bearer credentials in stdio headers (P2 in effect, filed P1): replaced the Bearer-only rule with a header-argument rule keyed on the header name, covering Authorization: Basic ... and X-API-Key: ... while leaving Accept: application/json intact.

Backup suite is 62 tests, make static-check green. PR body updated to describe the provider-option, symlink, and .git exclusions.

Mux is working on this PR on Mike's behalf.

@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: 8382ddbc66

ℹ️ 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/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

All three confirmed and fixed in b74a6e3, each red-green verified:

  • Merge provider options without replacing excluded providers (P1): mergeAiPreferences merges providerOptions provider by provider. The other ai fields keep replace semantics on purpose, because they are fully backed up; only data the backup could not carry is preserved.
  • Reject .git case-insensitively (P1): done, and I extended the same fix to FORBIDDEN_BASENAMES since it is the same defect class (skills/Providers.JSONC would have passed).
  • Stop header redaction consuming later arguments (P2): a regression I introduced last round. Header matching is now quote-aware, with the unquoted value stopping at whitespace, and an empty value left alone. The test asserts -H X-API-Key:hunter2 --transport stdio keeps its trailing flags.

Backup suite is 64 tests, make static-check green.

Mux is working on this PR on Mike's behalf.

@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: b74a6e3888

ℹ️ 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/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Fixed in 2b94f45. This was a straight violation of the self-healing rule you cited: rehydration parsed the local mcp.jsonc unconditionally, so the file most in need of repair blocked the repair. restoreMcpFile now returns early when the backup has no redaction marker, and a local parse failure is treated as no recoverable values. Preview shares the helper, so it agrees.

The guarantee is pinned by "restores over a malformed local MCP config", red-green verified by removing the try/catch. I also wrote a test for the early return and then deleted it: with the try/catch in place that branch is behaviorally unobservable, so the test passed with the code removed. I would rather have no test than one that cannot fail.

Backup suite is 65 tests, make static-check green.

Mux is working on this PR on Mike's behalf.

@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: 2b94f4549e

ℹ️ 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/payload.ts Outdated
Comment thread src/node/services/backup/payload.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Both confirmed and fixed in 64baf00, each red-green verified:

  • Reject duplicate MCP keys before redaction (P1): I took the reject option rather than tree-based redaction, since a duplicate key is already ambiguous config and refusing to export beats promising a redaction I cannot prove complete. assertNoDuplicateKeys walks the parseTree and is wired into parseJsoncObject, so export, read-back, and restore validation share it. The rehydration read of the local file keeps its try/catch, so a local duplicate still cannot block the restore that replaces it.
  • Reject case-colliding manifest paths (P2): case-folded in both readBackupPayload's duplicate check and the restore preflight. The test asserts the restore root is still empty, so nothing is written before the rejection.

Backup suite is 67 tests, make static-check green.

Mux is working on this PR on Mike's behalf.

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
…ify)

Scoped to the Codex-fix commits plus the CI test-infra fixes. Notable:
BackupSection no longer blocks its initial settings load on config
subscription establishment; it refreshes again once subscribed so changes
made during setup are still picked up (regression tests added).
Codex round 7:
1. Validate the remote managed tree before checkout: enumerate it offline
   via ls-tree (blob:none already fetched the trees), refuse trees over the
   file-count limit, size present blobs with lazy fetch disabled, prefetch
   missing blobs in small batches through the credential ladder, and enforce
   per-file and running-total limits between batches. Direct OID wants
   bypass --filter=blob:limit (verified empirically), so batching is the
   bound. The refusal is a recognized BackupInvalidPayloadError so a bloated
   repository does not trigger a cache rebuild loop.
2. The initial settings snapshot no longer marks settings fresh, so
   destructive actions stay disabled until a refresh runs with the config
   subscription armed (or subscription setup definitively fails). Closes the
   mount-time window where another window's repository change could enable
   Restore against a stale tuple.
…xity

Codex round 7b:
1. Every networkGit invocation now defaults to a 16 MiB output cap
   (MAX_NETWORK_GIT_OUTPUT_BYTES): remotes, SSH transports, and credential
   helpers are untrusted input and could otherwise buffer unbounded
   diagnostics in the main process. lsRemote keeps its tighter 1 MiB cap,
   and overflow classifies as the existing remote-unreachable error.
2. Payload paths are bounded by MAX_BACKUP_PATH_DEPTH (24 components) and
   MAX_BACKUP_DIRECTORY_COUNT (4096 distinct directory prefixes), enforced
   during manifest parsing, publication, restore planning, local collection,
   and the pre-checkout managed-tree validation, so a manifest within the
   byte and file-count limits cannot encode millions of directory
   components for Preview to lstat or Restore to create.

The local snapshot path keeps count and complexity checks only: snapshot
names preserve current-filesystem forms that portable validation refuses,
and collection already validated them under local rules.
Codex round 7c: --depth=1 alongside --filter=blob:none on both the cache
clone and the branch fetch bounds unvalidated transfer to the tip commit
(222-commit/2000-file fixture: 667 objects and 73 KiB down to 4 objects
and 6 KiB). Blob prefetch adds --refetch, which a shallow partial clone
requires to receive promised blobs by OID without deepening history.
tree:0 was evaluated and rejected: offline ls-tree breaks and direct
tree-OID refetch pulls the whole outside tree anyway, so the tip's tree
objects remain the accepted residual.
…empty-dir bounds

Codex round 8:
1. P1: a save response is a snapshot from before it was handled, so the
   handler no longer marks it fresh; freshness comes from re-reading the
   configuration after the save, closing the window where another window's
   concurrent save could leave Restore enabled against a stale repository.
2. Gitlinks under the managed path are refused before checkout as a
   recognized invalid payload: collection only emits regular files, so no
   legitimate backup contains submodules, and each mode-160000 entry would
   otherwise materialize an uncounted directory.
3. The cache object store is bounded (MAX_BACKUP_CACHE_OBJECT_KIB, 512 MiB)
   after clone, fetch, and each blob-prefetch batch via count-objects: git
   has no client-side received-bytes cap and tree filters break the offline
   ls-tree contract, so the subprocess timeout bounds transfer duration and
   this bounds retained object data.
4. Local collection charges each directory to the complexity budget before
   recursing, so broad empty-directory trees can no longer bypass
   MAX_BACKUP_DIRECTORY_COUNT, and directory paths get the same depth check
   as file paths.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@ibetitsmike
ibetitsmike force-pushed the mike/backup-sync-dbgh branch from 94e42b8 to edc1190 Compare August 7, 2026 23:23

@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: edc1190801

ℹ️ 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/features/Settings/Sections/BackupSection.tsx
…bounds

Codex round 9:
1. When the config change stream dies after freshness was established, the
   section clears settingsFresh: a dead stream cannot report another
   window's repository change, so destructive actions stop trusting the
   loaded tuple while settings stay visible.
2. An object-store budget overflow now discards the cache before the error
   surfaces; retries start from a fresh clone instead of fetching into an
   arbitrarily oversized store. Other invalid-payload refusals keep their
   small, reusable caches.
3. Manifest MCP redaction metadata is bounded (256 redactions, 64 segments
   per path, 2048 cumulative) before Sets or serialized paths are built,
   so a near-cap manifest cannot expand into hundreds of MB of derived
   structures during preview.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 4c0d5f8cb3

ℹ️ 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/features/Settings/Sections/BackupSection.tsx
Comment thread src/node/services/backup/gitRepo.ts
Codex round 10:
1. When config subscription setup fails, the fallback read still renders
   the latest settings but never grants freshness: with no listener, this
   window cannot see another window's later repository change, so
   destructive actions stay disabled rather than trusting an unwatchable
   snapshot.
2. Inactive repository caches are reaped after each successful prepare:
   a process-global registry of active cache basenames protects in-flight
   operations, the current cache plus the two most recently used inactive
   caches are retained (quick A/B switching avoids re-clones), and older
   caches go through the crash-safe rename-to-tombstone path. mtime only
   orders candidates already proven inactive.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 2979fd6ee1

ℹ️ 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/features/Settings/Sections/BackupSection.tsx
Comment thread src/node/services/backup/gitRepo.ts Outdated
…tree

Codex round 11:
1. Freshness is now granted only while the config stream is live, decided
   inside refresh itself, so the post-save re-read (and any other refresh)
   cannot re-enable destructive actions after subscription setup failed or
   the stream died.
2. The post-materialize clean covers the whole cache worktree instead of
   only the current managed path: the cache identity is repository and
   branch, so exports written under a previously configured subdirectory
   would otherwise accumulate outside every later sparse checkout. The
   glob-literal test now pins removal of strays while sparse and staging
   pathspecs stay literal; the unsafe-managed-path test vehicle moved to
   stageAndCommit.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 4955283e67

ℹ️ 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/backupService.ts Outdated
Comment thread src/browser/features/Settings/Sections/BackupSection.tsx
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
Reap inactive backup caches even when repository preparation rejects, while preserving the failed cache and original error. Scope config-stream liveness to each API subscription so replacement clients cannot inherit or overwrite another generation's freshness.

---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh -->
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 0e76a2bb0b

ℹ️ 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".

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