[Bug]: Advanced background activity changes fail with "Setting not saved"#17233

opened17d agobyBbfowler

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Open the desktop app and select a connected environment in Settings → General.
  2. Under Background activity, choose Advanced.
  3. Turn off Pause when locked.

Observed with the default balanced profile and permission to change settings. The steps were performed by the reporter; the server failure was also reproduced independently through the real RPC authorization middleware without changing live settings.

Expected behavior

The setting is saved, Pause when locked remains off, and the advanced settings dialog stays open until dismissed.

Actual behavior

The dialog closes and an error toast says Setting not saved / Could not update [environment name]. The setting is not saved. A transport connection loss and interrupted save request are logged immediately afterward, and the app reconnects.

This fails before the settings save handler runs. It is not a denied settings permission: the isolated reproduction grants AuthSettingsWriteScope.

Impact

Major degradation or frequent failure

Advanced background activity settings cannot be saved through this path. Repeated attempts were accompanied by interruption of the environment connection.

Version or commit

Desktop: 0.0.46-nightly.20261008.2819.

Independent server-boundary reproduction: checkout based on main @ 9a3070bcf023c3c025633659addeef4cf628133a. The introducing commit has not been identified.

Environment

Windows x64, T3 Code Nightly desktop, local environment. Exact Windows release was not collected. The isolated reproduction used Node.js 24.15.0 and the checkout's installed dependencies. Provider independent.

Logs or stack traces

The retained trace contains the same error at 11:55:50, 11:55:59, and 11:57:48 local time on 2026-10-08:

text
1SchemaError: Expected number
2 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]

Each is followed by EnvironmentSupervisor.connectionLost with transport failure and an interrupted RpcClient.server.updateSettings request. Raw traces are omitted because they contain authentication data.

Cause: RPC has already decoded the request before invoking its authorization middleware. Schema.DurationFromMillis has therefore converted millisecond numbers into Duration objects. However, requiredScopesForSettingsUpdate calls Schema.decodeUnknownSync(SettingsUpdate)(payload) again. That second decode expects wire-format numbers and throws on a duration object.

The checkbox calls backgroundActivityOverrideSettings, which includes all resolved timer values even when changing one boolean. The first rejected timer is automaticGitFetchInterval.

An isolated RpcTest.makeClient reproduction used the actual WsRpcGroup settings method, actual RpcAuthorization.layer([AuthSettingsWriteScope]), and actual advanced-settings helper. The save handler only marked whether it ran and returned DEFAULT_SERVER_SETTINGS; it performed no filesystem or live-state writes:

  • Simple boolean settings patch: succeeds; handler runs.
  • Background policy containing only pauseWhenHostLocked: false and profile/base profile: succeeds; handler runs.
  • Actual advanced-checkbox helper output: fails with the exact error above; handler never runs.

The exact RPC payload also encodes successfully using both the ordinary schema encoder and its JSON codec. This establishes a server authorization double-decode error rather than a client encoding error.

An independent Claude Opus 5.5 review confirmed that the framework decodes the payload before middleware runs and reproduced the same second-decode failure using the real contracts. Neither diagnostic reproduction was an end-to-end run of the shipped desktop client; the desktop symptoms and traces came from the reporter's running app.

The same helper is used by interval controls, so the code indicates a broader effect on settings patches containing durations, including the top-level Git fetch and provider refresh intervals. Those additional UI controls were not tested individually.

The dialog's open state depends on settings-write availability, and the settings route removes its children while the selected environment is disconnected. This is consistent with the observed closure after the connection loss. The exact path from the middleware defect to transport teardown was not independently reproduced.

Screenshots, recordings, or supporting files

The reporter supplied a screenshot of the toast; its text is transcribed above.

Workaround

No supported UI workaround was verified. Preset profiles do not provide the requested setting: all three presets pause when the host is locked. The isolated boolean-only request is diagnostic evidence, not a user-facing workaround.

The authorization middleware should inspect or validate the already-decoded settings type instead of reapplying the wire decoder, while preserving its settings/provider scope checks. A regression test should exercise a settings update containing a duration through the actual authorization middleware. No fix has been applied.

Duplicate search covered open and closed issues using the UI labels, exact error, duration field, and authorization function names. #15131 concerns the MCP preferences tool's missing ThreadCommandExecutor; it is a different path and cause.

Investigated by GPT-6.1-Sol through the Codex harness; independently reviewed by Claude Opus 5.5 through the Claude Agent provider.

Note

Grok responding on behalf of Julius.

Thanks for the detailed report and the isolated reproduction. The double decode checks out on main @ a6ec88f7a.

What's happening

  • requiredScopesForSettingsUpdate runs Schema.decodeUnknownSync(SettingsUpdate)(payload) inside the RpcScopeAuthorization middleware (apps/server/src/auth/RpcAuthorization.ts:227-240, called from :259 and :266-271). By then, the RPC server has already decoded payload against the same struct, which is the serverUpdateSettings payload schema (packages/contracts/src/rpc.ts:737-741).
  • BackgroundActivityOverrides uses Schema.DurationFromMillis for all five timers (packages/contracts/src/settings.ts:1128-1133). The top-level automaticGitFetchInterval / providerHealthRefreshInterval in ServerSettingsPatch use it too (settings.ts:1747-1748). The second decode gets Duration values where it expects millisecond numbers and throws. I reproduced this with a minimal struct on effect 4.0.1 (the version in the workspace catalog): the first decode succeeds, the second throws SchemaError: Expected number at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"], and a boolean-only override patch decodes twice without error.
  • backgroundActivityOverrideSettings always copies every resolved timer into overrides. That's why flipping any Advanced toggle, including Pause when locked, sends Durations and hits this.

Why the connection drops (likely) The throw is synchronous inside the middleware function, not an Effect.fail. In effect 4.0.1's RpcServer, middleware is applied inside handleRequest, so the throw appears to reach the Effect.catchDefect around write and is sent as a connection-level Defect instead of a per-request exit. On the client, RpcClient handles Defect with clearEntries(Exit.die(...)), which fails every pending request and open stream on that socket. That fits the connectionLost → reconnect you saw. The dialog closing is probably a knock-on effect of the environment briefly disconnecting, but I didn't trace that UI path end to end.

Scope

  • This most likely started with #9786 (merged 2026-10-06), which added the SettingsUpdate re-decode. Before that, serverUpdateSettings had a fixed scope (AuthOrchestrationOperateScope) and its payload wasn't decoded again. The existing tests in RpcAuthorization.test.ts only send patches without Durations, so they didn't catch it.
  • It probably also affects the Git fetch and provider health check interval inputs and their reset buttons, which go through the same helper. Restore defaults in General likely hits it too, since it sends the top-level Duration fields. I haven't tested those individually.
  • assetsCreateUrl and gitPreparePullRequestThread use the same "decode the payload again in the middleware" pattern. Their schemas look idempotent at a glance, so I don't think they're affected today, but I haven't verified that.

Possible fix (untested)

  • requiredScopesForServerSettingsPatch only looks at which keys are defined (settings.ts:1830-1846), and the payload is already the decoded type. So the middleware could treat payload as typeof SettingsUpdate.Type, or check it with a type-side guard, instead of running the wire decoder again.
  • Separately, it may be worth having the middleware turn any unexpected throw into a typed failure, so a bug in scope calculation fails one request instead of the whole connection.
  • A regression test would help: an RpcTest.makeClient call through RpcAuthorization.layer([AuthSettingsWriteScope]) with a backgroundActivity.overrides patch that contains a Duration.

Related I didn't find a duplicate or an open fix PR. #15131 (MCP preferences tool missing ThreadCommandExecutor) is a different path. #13235 and #12000 are about how the Git fetch and provider health intervals behave, not about saving them.

Workaround until a fix lands: choosing a preset profile should still save, since it sends overrides: {}. As you noted, though, none of them turns off Pause when locked.

Synced from GitHub

Confirming this on T3 Code (Beta) 0.1.0, macOS 26.6.2, local desktop environment.

It also reproduces from the Provider health check interval input under Settings → Providers. The comment above listed that path as likely but untested. Every attempt fails the same way. The server trace logs have 13 occurrences across about 20 minutes of retries:

1SchemaError: Expected number
2 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]

Each attempt runs the same sequence:

  1. The client server.updateSettings span is interrupted about 5–7ms after it is sent.
  2. Every open subscription on the socket is interrupted at the same moment.
  3. EnvironmentSupervisor.connectionLost fires, and the UI shows "Reconnect to change its settings" or "Could not save on : All fibers interrupted without error".
  4. The environment reconnects a few seconds later.

settings.json never gains a backgroundActivity key. Saving provider instances and project overrides works normally. This is consistent with the double-decode analysis above. It looks like #18129 would fix it.

Synced from GitHub

Same here on Linux. Reproduces from Settings → Providers → Advanced → Health check interval: as soon as I edit the value (both with plus/minus buttons and the number directly), the panel switches to "Reconnect to change its settings", the local environment drops, and the value goes back to 300. In my case the environment stayed disconnected for roughly 80 seconds before coming back, longer than the "few seconds" reported above. Screen recording attached.

My server trace shows the same error:

1SchemaError: Expected number
2 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]

Environment

  • T3 Code 0.1.0 (desktop, Arch package t3code-bin 0.1.0-1 from AUR, release channel), local environment
  • Arch Linux, kernel 7.2.11-arch1-1
  • KDE Plasma 6.8.0 on Wayland
  • Providers: Codex v0.152.0, Claude v2.1.220, OpenCode v1.18.25

Happy to volunteer as a Linux tester for #18129 or any other fix if that helps, just ping me.

https://github.com/user-attachments/assets/ef152afe-807f-4fd3-8631-8ee6c25c984a

Synced from GitHub

Confirming the Provider health check interval reproduction on T3 Code (Beta) 0.1.0, macOS 27.0.1 arm64, local desktop environment.

With the Performance background profile, changing Settings → Providers → Advanced → Health check interval from 60 to 600 seconds fails through both typing and the step buttons. The environment connection drops, settings controls temporarily show the reconnect message, and the saved value remains 60 seconds. A full app restart does not resolve it.

The retained trace contains the same failure reported here:

text
1SchemaError: Expected number
2 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]

I independently checked #18129 against the exact health-refresh case, including numeric millisecond serialization, real settings persistence in a temporary home, and two missing-permission cases. All three new cases fail on main 7e289ae1f77b9137580688aaccf00d48906984ad and pass on PR head e8eb1110ce9f3de021bfa2d56a57a273e807ff4c; the candidate's existing 26 authorization tests also pass.

Full verification, runnable fixture, platform/toolchain limitations, and current merge-conflict finding.

Raw traces are omitted because they contain authentication material. No installed build or live state was patched. The existing PR still requires its human authorization review and an update for the current main conflict.

Reproduction and verification: GPT-6 through Codex Desktop, on behalf of @OscarSwanros.

Existing fix: #18129. Its current description includes failing-main regression results and live Linux before/after evidence, and identifies human authorization review as the remaining gate. This triage did not repeat runtime verification or change code: the proposed fix changes RPC authorization scope calculation, which requires maintainer handling under the triage rules. Leaving this issue open; no duplicate PR created.

Reproduced on current main 49e97d48f9 with an isolated Linux dev server and the web client: Settings → General → Background activity → Advanced → turn off “Pause when host is locked”. The dialog closes and the environment disconnects.

A separate request through the running server’s authenticated WebSocket endpoint reads settings successfully, then the Advanced duration-bearing patch returns a connection-level Defect: SchemaError: Expected number at backgroundActivity.overrides.automaticGitFetchInterval.

Existing work in #18129 is retained. This is a settings-save bug; touching authorization does not classify it as a reported exploitable vulnerability.

Synced from GitHub

Confirming this on T3 Code 0.1.1 stable, Linux x64, Firefox, connecting to a local background service installed through the terminal installer.

Reproduction

  1. Open Settings → General → Background Activity → Advanced.
  2. With the Performance profile selected, enable Pause on battery.
  3. Saving fails with: Could not save on <host>: All fibers interrupted without error.

This prevents configuring background work to pause while the laptop runs on battery.

Trace evidence

Two attempts on 2026-10-23 UTC show:

text
101:25:02.050 server.updateSettings
2 durationMs=27, effect.fiber.interrupted=true
301:25:02.073 EnvironmentSupervisor.connectionLost
4 connection.failure.reason=transport
5
601:29:41.111 server.updateSettings
7 durationMs=15, effect.fiber.interrupted=true
801:29:41.123 EnvironmentSupervisor.connectionLost
9 connection.failure.reason=transport

The environment reconnects afterward. The saved background activity configuration still has profile: "performance" and overrides: {}.

This matches the interrupted-save/connection-loss pattern already reported here. Source inspection confirms that both release tags v0.1.1 and v0.1.2 retain the Schema.decodeUnknownSync(SettingsUpdate)(payload) call in requiredScopesForSettingsUpdate. The exact schema exception was not captured in this investigation, so the underlying cause is inferred from the matching symptoms and code.

Tracking the existing fix in #18129. No local source or configuration changes were made. Raw logs and identifying host details are omitted.

Image

Prepared by GPT-6 through Codex during T3 Code triage.

Synced from GitHub

Confirming the same failure on T3 Code desktop 0.1.2, macOS arm64, affecting both the desktop app's local environment and a connected remote environment. This adds current-release and remote-environment evidence to this issue; the root cause matches the existing diagnosis and fixes in #18129 / #23342.

What happened

Changing Settings → Providers → Health check interval replaces the page with “Reconnect to change its settings.” Opening Providers directly also reproduced the problem during the original investigation. Renaming a provider instance and toggling its enabled state succeed.

Diagnosis

The installed 0.1.2 backend's settings authorization function applies the wire decoder to an RPC payload that has already been decoded. Numeric milliseconds have become Effect Duration values, so Schema.decodeUnknownSync(SettingsUpdate)(payload) rejects them. The health interval control sends multiple resolved durations in backgroundActivity.overrides, making automaticGitFetchInterval the first rejected field even when the provider interval is being changed.

An isolated reproduction executed the installed authorization function and installed RPC schemas: a valid encode/decode succeeds, the second decode fails, and a provider name/enabled patch passes. It used a 30-second provider interval and a 60-second Git interval; the original UI values were not captured. This was not an end-to-end desktop test.

Steps to reproduce

  1. Open T3 Code desktop 0.1.2 on macOS with the local environment connected.
  2. Select that environment under Settings → Providers.
  3. Change Health check interval to another valid value.
  4. Observe the connection failure and reconnect notice.
  5. Repeat with a connected remote environment selected in the same desktop app.
  6. Compare with renaming or enabling/disabling a provider instance; those changes succeed.

Expected: save the interval and keep the selected environment connected.

Version and environment

  • Desktop app and local server: 0.1.2. Remote server version: unknown.
  • macOS, Darwin 27.0.0, arm64; local and paired remote environments.
  • Triage CLI: 0.1.2 (npx t3 triage). Its context recorded Node v26.12.1; this does not establish the desktop's embedded Node version.

Evidence

Repeated local failures and two remote failures showed the same error. Representative local sequence on 2026-10-25 UTC:

text
114:30:34.0202 server.updateSettings starts; later interrupted
214:30:34.0303 subscribeServerConfig reports SchemaError:
3Expected number
4 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]
514:30:34.0335 EnvironmentSupervisor.connectionLost; reason=transport

Installed-build isolated check:

text
1PASS: provider instance rename/toggle
2FAIL: health interval change (matches live logs)
3Expected number
4 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]

The reporter has a screenshot of the reconnect notice to attach manually.

Fix applied or workaround

No live settings, services, database, or installed application source were changed. A settings-file workaround using numeric milliseconds at backgroundActivity.overrides.providerHealthRefreshInterval, with a custom profile retaining its previous base profile, was prepared and previewed only. Live reload and UI persistence were not verified; this is not a verified workaround.

Independent verification of existing fix #23342

Verified on Linux x64 with Node 26.12.1, Effect 4.0.5, and Vite+ 1.4.0. Starting from current main 5b35402623d7d1d88eaa472807272a6c2e78012a, I applied #23342's original regression test, then its exact Schema.toType(SettingsUpdate) correction (PR head adf0b49ede31d6f29354ae78df7eed90e7a9ee04). This tests the existing fix against today's main; it is not a run of the older PR checkout's entire dependency tree.

text
1vp test run --maxWorkers=1 apps/server/src/auth/RpcAuthorization.test.ts
2
3Main + PR regression: 16 passed, 1 failed
4 SchemaError: Expected number
5 at ["patch"]["backgroundActivity"]["overrides"]["automaticGitFetchInterval"]
6
7Main + existing fix + PR regression: 17 passed
8Main + seven supplemental verification cases, without fix: all seven fail
9Main + existing fix + all verification cases: 24 passed

The seven supplemental cases cover:

  • The actual backgroundActivityOverrideSettings Providers helper, all resolved timers, and real RPC-schema JSON encode/decode: Git interval serializes to 60000, provider interval to 30000.
  • The real authorization middleware and ServerSettingsService, persisting numeric intervals to settings.json in a temporary test home. A second update through the same in-memory RPC client persists 45000 and reads back successfully.
  • Four missing-permission combinations, including mixed provider/settings mutations: the expected typed settings:write or providers:manage error is returned and the handler is not called.
  • Both legacy top-level duration fields.

Production-file formatting passed. Lint reported only the pre-existing inline asset-decoder warning. An additional server-package typecheck was stopped because of load on the shared server; no successful typecheck result is claimed. The production change is small and preserves scope calculation; it corrects validation of the already-decoded payload.

Limits: the supplemental fixture uses RpcTest plus explicit JSON schema round-tripping, not a real WebSocket or desktop UI. No post-fix macOS, Electron, or remote/relay deployment was tested. The fix remains in an existing, unmerged PR; no duplicate issue or PR was opened and no installed build or live state was patched.

Original investigation: Codex (GPT-6) via t3 triage. Report organization, duplicate search, and independent verification: GPT-6.1-Sol through the Codex harness in T3 Code. Existing fix credited to #23342 and the prior work in #18129.