Skip to content

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

Description

@bfowler

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:

SchemaError: Expected number
  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.

Activity

  1. juliusmarminge commented on Oct 8, 2026

    @juliusmarminge
    Member

    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 feat(auth): separate environment administration permissions #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.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 8, 2026
  3. added a commit that references this issue on Oct 11, 2026
    cbbf7e5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions