# Files changed · PR \#23213 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23213/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(contracts\): older clients connect to servers with idle compaction modes](https://git.cafe/pingdotgg/t3code/pull/23213.md)

Version: 3

Head commit: de75c79a56bc5cc6b2e8da37e6c7be7ff5e612ee

Base commit: 89fe509232e787b51d0cf695861afe73316ce85b

Comparison base: ed9b7ca9c53843ea1e6b466bf7d52928d10e7db0

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/serverSettings.test.ts

Status: modified; +17; −0

```diff
@@ -212,6 +212,23 @@
     }).pipe(Effect.provide(layerServerSettings())),
   );
 
+  it.effect("keeps idle compaction off across unrelated edits and on disk", () =>
+    Effect.gen(function* () {
+      const config = yield* ServerConfig.ServerConfig;
+      const fs = yield* FileSystem.FileSystem;
+      const service = yield* ServerSettingsModule.ServerSettingsService;
+
+      yield* service.updateSettings({ compactIdleThreads: "off" });
+      const next = yield* service.updateSettings({ sidebarAutoSettleOnMerge: false });
+
+      assert.equal(next.compactIdleThreads, "off");
+      const persisted = yield* decodeServerSettingsJson(
+        yield* fs.readFileString(config.settingsPath),
+      );
+      assert.equal(persisted.compactIdleThreads, "off");
+    }).pipe(Effect.provide(layerServerSettings())),
+  );
+
   it.effect("saves through a symlinked settings file without replacing the link", () =>
     Effect.gen(function* () {
       const config = yield* ServerConfig.ServerConfig;
```

## packages/contracts/src/rpc.test.ts

Status: modified; +22; −0

```diff
@@ -4,7 +4,11 @@
 
 import { ORCHESTRATION_V2_WS_METHODS } from "./orchestrationV2.ts";
 import { WS_METHODS, WsRpcGroup } from "./rpc.ts";
+import { ServerConfigSettingsUpdatedPayload } from "./server.ts";
+import { DEFAULT_SERVER_SETTINGS } from "./settings.ts";
 
+const encodeSettingsUpdatedPayload = Schema.encodeSync(ServerConfigSettingsUpdatedPayload);
+
 describe("WebSocket RPC contracts", () => {
   it("rejects server-internal commands sent to dispatchCommand", () => {
     const dispatchCommand = WsRpcGroup.requests.get(ORCHESTRATION_V2_WS_METHODS.dispatchCommand);
@@ -44,4 +48,22 @@
     expect(Exit.isSuccess(decode({ ...request, path: null }))).toBe(true);
     expect(Exit.isFailure(decode({ ...request, path: "/repo/.git/hooks" }))).toBe(true);
   });
+
+  it("sends compactIdleThreads as the boolean older clients decode", () => {
+    const settings = { ...DEFAULT_SERVER_SETTINGS, compactIdleThreads: "off" as const };
+    const successSchemas = [WS_METHODS.serverGetSettings, WS_METHODS.serverUpdateSettings].map(
+      (method) => {
+        const rpc = WsRpcGroup.requests.get(method);
+        if (rpc === undefined) throw new Error(`${method} is not registered`);
+        return rpc.successSchema;
+      },
+    );
+    const encoded = [
+      ...successSchemas.map((schema) => Schema.encodeUnknownSync(schema)(settings)),
+      encodeSettingsUpdatedPayload({ settings }).settings,
+    ];
+    for (const value of encoded) {
+      expect(value).toMatchObject({ compactIdleThreads: false, idleCompactionMode: "off" });
+    }
+  });
 });
```

## packages/contracts/src/rpc.ts

Status: modified; +3; −3

```diff
@@ -334,7 +334,7 @@
 import {
   StorageCleanupReport,
   DatabaseStorageStatus,
-  ServerSettings,
+  ServerSettingsWire,
   ServerSettingsError,
   ServerSettingsPatch,
 } from "./settings.ts";
@@ -757,7 +757,7 @@
 
 const WsServerGetSettingsRpc = Rpc.make(WS_METHODS.serverGetSettings, {
   payload: Schema.Struct({}),
-  success: ServerSettings,
+  success: ServerSettingsWire,
   error: Schema.Union([ServerSettingsError, EnvironmentAuthorizationError]),
 });
 
@@ -766,7 +766,7 @@
     patch: ServerSettingsPatch,
     providerInstanceMutation: Schema.optionalKey(ProviderInstanceMutation),
   }),
+  success: ServerSettingsWire,
-  success: ServerSettings,
   error: Schema.Union([ServerSettingsError, EnvironmentAuthorizationError]),
 });
 
```

## packages/contracts/src/server.ts

Status: modified; +4; −4

```diff
@@ -27,7 +27,7 @@
 import { RuntimeMode } from "./providerPolicy.ts";
 import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts";
 import { ServerProviderUsageLimits, UsageLimitSourceSnapshots } from "./providerUsageLimits.ts";
-import { ServerSettings } from "./settings.ts";
+import { type ServerSettings, ServerSettingsWire } from "./settings.ts";
 
 const KeybindingsMalformedConfigIssue = Schema.Struct({
   kind: Schema.Literal("keybindings.malformed-config"),
@@ -615,7 +615,7 @@
    */
   directEndpoints: Schema.optionalKey(ForwardCompatibleArray(ServerDirectEndpoint)),
   observability: ServerObservability,
-  settings: ServerSettings,
+  settings: ServerSettingsWire,
   shellResumeCompletionMarker: Schema.optionalKey(Schema.Boolean),
   shellRevealInFileManager: Schema.optionalKey(Schema.Boolean),
   shellRevealInFileManagerKind: Schema.optionalKey(FileManagerRevealKind),
@@ -687,7 +687,7 @@
 export const ServerConfigUpdatedPayload = Schema.Struct({
   issues: ServerConfigIssues,
   providers: ServerProviders,
-  settings: Schema.optional(ServerSettings),
+  settings: Schema.optional(ServerSettingsWire),
 });
 export type ServerConfigUpdatedPayload = typeof ServerConfigUpdatedPayload.Type;
 
@@ -704,7 +704,7 @@
 export type ServerConfigProviderStatusesPayload = typeof ServerConfigProviderStatusesPayload.Type;
 
 export const ServerConfigSettingsUpdatedPayload = Schema.Struct({
-  settings: ServerSettings,
+  settings: ServerSettingsWire,
 });
 export type ServerConfigSettingsUpdatedPayload = typeof ServerConfigSettingsUpdatedPayload.Type;
 
```

## packages/contracts/src/settings.test.ts

Status: modified; +52; −0

```diff
@@ -1,4 +1,5 @@
 import { describe, expect, it } from "vite-plus/test";
+import * as Effect from "effect/Effect";
 import * as Schema from "effect/Schema";
 
 import { type CommandId, ProjectId } from "./baseSchemas.ts";
@@ -14,6 +15,7 @@
   resolveProviderInstanceEnabled,
   ServerSettings,
   ServerSettingsPatch,
+  ServerSettingsWire,
 } from "./settings.ts";
 
 const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema);
@@ -22,6 +24,9 @@
 const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);
 const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);
 const encodeServerSettings = Schema.encodeSync(ServerSettings);
+const encodeServerSettingsPatch = Schema.encodeSync(ServerSettingsPatch);
+const decodeServerSettingsWire = Schema.decodeUnknownSync(ServerSettingsWire);
+const encodeServerSettingsWire = Schema.encodeSync(ServerSettingsWire);
 const decodeClaudeSettings = Schema.decodeUnknownSync(ClaudeSettings);
 
 describe("storage cleanup settings", () => {
@@ -157,6 +162,53 @@
     );
     expect(decodeServerSettings({}).compactIdleThreads).toBe("onReturn");
   });
+
+  // The field as clients before idle compaction modes (680152e27b) decode it.
+  const decodeLegacyClientSettings = Schema.decodeUnknownSync(
+    Schema.Struct({
+      compactIdleThreads: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
+    }),
+  );
+
+  it.each(["onReturn", "beforeCacheExpires", "off"] as const)(
+    "sends %s in a shape older clients decode and newer ones restore",
+    (mode) => {
+      const encoded = encodeServerSettingsWire(decodeServerSettings({ compactIdleThreads: mode }));
+
+      expect(decodeLegacyClientSettings(encoded).compactIdleThreads).toBe(
+        mode === "beforeCacheExpires",
+      );
+      expect(decodeServerSettingsWire(encoded).compactIdleThreads).toBe(mode);
+    },
+  );
+
+  it("reads compactIdleThreads from servers that send only the boolean or the mode", () => {
+    expect(decodeServerSettingsWire({ compactIdleThreads: true }).compactIdleThreads).toBe(
+      "beforeCacheExpires",
+    );
+    expect(decodeServerSettingsWire({ compactIdleThreads: false }).compactIdleThreads).toBe(
+      "onReturn",
+    );
+    expect(decodeServerSettingsWire({ compactIdleThreads: "off" }).compactIdleThreads).toBe("off");
+    expect(decodeServerSettingsWire({}).compactIdleThreads).toBe("onReturn");
+  });
+
+  it("accepts the legacy switch in settings patches and round-trips every mode", () => {
+    expect(decodeServerSettingsPatch({ compactIdleThreads: true }).compactIdleThreads).toBe(
+      "beforeCacheExpires",
+    );
+    expect(decodeServerSettingsPatch({ compactIdleThreads: false }).compactIdleThreads).toBe(
+      "onReturn",
+    );
+    for (const mode of ["onReturn", "beforeCacheExpires", "off"] as const) {
+      const encoded = encodeServerSettingsPatch({ compactIdleThreads: mode });
+      expect(decodeServerSettingsPatch(encoded).compactIdleThreads).toBe(mode);
+    }
+    const unrelated = decodeServerSettingsPatch(
+      encodeServerSettingsPatch({ sidebarAutoSettleOnMerge: false }),
+    );
+    expect(unrelated).not.toHaveProperty("compactIdleThreads");
+  });
 });
 
 describe("ClientSettings notifications", () => {
```

## packages/contracts/src/settings.ts

Status: modified; +34; −1

```diff
@@ -1321,6 +1321,39 @@
 });
 export type ServerSettings = typeof ServerSettings.Type;
 
+/**
+ * `ServerSettings` as it crosses the wire. Clients from before idle compaction
+ * modes reject anything but a boolean `compactIdleThreads`, so the mode travels
+ * in `idleCompactionMode` beside it. Servers from before this split send only
+ * `compactIdleThreads`, as a boolean or a mode.
+ */
+export const ServerSettingsWire = Schema.Struct({
+  ...ServerSettings.fields,
+  compactIdleThreads: Schema.optionalKey(Schema.Union([Schema.Boolean, IdleCompactionMode])),
+  idleCompactionMode: Schema.optionalKey(IdleCompactionMode),
+}).pipe(
+  Schema.decodeTo(
+    Schema.toType(ServerSettings),
+    SchemaTransformation.transform({
+      decode: ({ compactIdleThreads, idleCompactionMode, ...settings }) => ({
+        ...settings,
+        compactIdleThreads:
+          idleCompactionMode ??
+          (typeof compactIdleThreads === "boolean"
+            ? compactIdleThreads
+              ? "beforeCacheExpires"
+              : "onReturn"
+            : (compactIdleThreads ?? "onReturn")),
+      }),
+      encode: ({ compactIdleThreads, ...settings }) => ({
+        ...settings,
+        compactIdleThreads: compactIdleThreads === "beforeCacheExpires",
+        idleCompactionMode: compactIdleThreads,
+      }),
+    }),
+  ),
+);
+
 export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({});
 
 export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | undefined => {
@@ -1535,7 +1568,7 @@
   deviceHosts: Schema.optionalKey(SshDeviceHostConfigs),
   easDeviceHosts: Schema.optionalKey(EasDeviceHostConfigs),
   sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),
-  compactIdleThreads: Schema.optionalKey(IdleCompactionMode),
+  compactIdleThreads: Schema.optionalKey(IdleCompactionModeSetting),
   sidebarAutoSettleKeepPullRequests: Schema.optionalKey(Schema.Boolean),
   sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean),
   autoResumeLimitedThreads: Schema.optionalKey(Schema.Boolean),
```
