From 21a1242530eb8038d7c51755d9ea00703e1c9315 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 16:08:52 +0000 Subject: [PATCH 1/2] Use idiomatic Effect readiness policies Co-authored-by: Julius Marminge --- apps/server/src/vcs/VcsProjectConfig.ts | 27 ++++---------- packages/ssh/src/tunnel.test.ts | 6 +-- packages/ssh/src/tunnel.ts | 49 +++++++++++++------------ 3 files changed, 37 insertions(+), 45 deletions(-) diff --git a/apps/server/src/vcs/VcsProjectConfig.ts b/apps/server/src/vcs/VcsProjectConfig.ts index 3e5ee2347ce..13489fa09d4 100644 --- a/apps/server/src/vcs/VcsProjectConfig.ts +++ b/apps/server/src/vcs/VcsProjectConfig.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; @@ -15,16 +16,9 @@ const ProjectVcsConfig = Schema.Struct({ ), vcsKind: Schema.optional(VcsDriverKind), }); -const isProjectVcsConfig = Schema.is(ProjectVcsConfig); - -interface ProjectVcsConfigFile { - readonly vcs?: - | { - readonly kind?: VcsDriverKindType | undefined; - } - | undefined; - readonly vcsKind?: VcsDriverKindType | undefined; -} +const ProjectVcsConfigJson = Schema.fromJsonString(ProjectVcsConfig); +const decodeProjectVcsConfigJson = Schema.decodeUnknownOption(ProjectVcsConfigJson); +type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type; export interface VcsProjectConfigResolveInput { readonly cwd: string; @@ -45,13 +39,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto return config.vcs?.kind ?? config.vcsKind ?? "auto"; } -function parseConfig(raw: string): ProjectVcsConfigFile | null { - try { - const parsed = JSON.parse(raw) as unknown; - return isProjectVcsConfig(parsed) ? parsed : null; - } catch { - return null; - } +function parseConfig(raw: string): Option.Option { + return decodeProjectVcsConfigJson(raw); } export const make = Effect.fn("makeVcsProjectConfig")(function* () { @@ -90,14 +79,14 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { } const parsed = parseConfig(raw); - if (parsed === null) { + if (Option.isNone(parsed)) { yield* Effect.logWarning("invalid VCS project config", { configPath, }); return "auto" as const; } - return configuredKind(parsed); + return configuredKind(parsed.value); }); const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 80e684d8611..a2e7ce86963 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -242,9 +242,9 @@ describe("ssh tunnel scripts", () => { Effect.result( waitForHttpReady({ baseUrl: "http://127.0.0.1:41773/", - timeoutMs: 1_000, - intervalMs: 100, - probeTimeoutMs: 250, + timeout: Duration.seconds(1), + interval: Duration.millis(100), + probeTimeout: Duration.millis(250), }), ), ); diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..20c983b8497 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -50,11 +50,13 @@ import { export const DEFAULT_REMOTE_PORT = 3773; const REMOTE_PORT_SCAN_WINDOW = 200; -const SSH_READY_TIMEOUT_MS = 20_000; -const SSH_READY_PROBE_TIMEOUT_MS = 1_000; -const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; -const REMOTE_READY_TIMEOUT_MS = 15_000; -const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; +const SSH_READY_TIMEOUT = Duration.seconds(20); +const SSH_READY_PROBE_TIMEOUT = Duration.seconds(1); +const TUNNEL_SHUTDOWN_TIMEOUT = Duration.seconds(2); +const REMOTE_READY_TIMEOUT = Duration.seconds(15); +const REMOTE_REUSE_READY_TIMEOUT = Duration.seconds(2); +const DEFAULT_HTTP_READY_TIMEOUT = Duration.seconds(30); +const HTTP_READY_RETRY_INTERVAL = Duration.millis(100); export interface RemoteT3RunnerOptions { readonly packageSpec?: string; @@ -686,9 +688,9 @@ export function buildRemoteLaunchScript(input?: RemoteT3RunnerOptions): string { T3_WAIT_READY_SCRIPT: stripTrailingNewlines(REMOTE_WAIT_READY_SCRIPT), T3_DEFAULT_REMOTE_PORT: String(DEFAULT_REMOTE_PORT), T3_REMOTE_PORT_SCAN_WINDOW: String(REMOTE_PORT_SCAN_WINDOW), - T3_READY_TIMEOUT_MS: String(REMOTE_READY_TIMEOUT_MS), - T3_REUSE_READY_TIMEOUT_MS: String(REMOTE_REUSE_READY_TIMEOUT_MS), - T3_READY_PROBE_TIMEOUT_MS: String(SSH_READY_PROBE_TIMEOUT_MS), + T3_READY_TIMEOUT_MS: String(Duration.toMillis(REMOTE_READY_TIMEOUT)), + T3_REUSE_READY_TIMEOUT_MS: String(Duration.toMillis(REMOTE_REUSE_READY_TIMEOUT)), + T3_READY_PROBE_TIMEOUT_MS: String(Duration.toMillis(SSH_READY_PROBE_TIMEOUT)), }); } @@ -870,17 +872,18 @@ const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")( export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(function* (input: { readonly baseUrl: string; - readonly timeoutMs?: number; - readonly intervalMs?: number; - readonly probeTimeoutMs?: number; + readonly timeout?: Duration.Input; + readonly interval?: Duration.Input; + readonly probeTimeout?: Duration.Input; readonly path?: string; }): Effect.fn.Return { - const timeoutMs = input.timeoutMs ?? 30_000; - const intervalMs = input.intervalMs ?? 100; - const probeTimeoutMs = input.probeTimeoutMs ?? SSH_READY_PROBE_TIMEOUT_MS; - const retryPolicy = Schedule.spaced(Duration.millis(intervalMs)).pipe( - Schedule.take(Math.max(0, Math.ceil(timeoutMs / intervalMs))), - ); + const timeout = input.timeout ?? DEFAULT_HTTP_READY_TIMEOUT; + const interval = input.interval ?? HTTP_READY_RETRY_INTERVAL; + const probeTimeout = input.probeTimeout ?? SSH_READY_PROBE_TIMEOUT; + const timeoutMs = Duration.toMillis(timeout); + const intervalMs = Duration.toMillis(interval); + const probeTimeoutMs = Duration.toMillis(probeTimeout); + const retryPolicy = Schedule.spaced(interval); const requestUrl = new URL(input.path ?? "/", input.baseUrl).toString(); const client = yield* HttpClient.HttpClient; const lastProbeFailure = yield* Ref.make(null); @@ -900,7 +903,7 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio Effect.gen(function* () { attempt += 1; const responseOption = yield* effect.pipe( - Effect.timeoutOption(Duration.millis(probeTimeoutMs)), + Effect.timeoutOption(probeTimeout), Effect.mapError( (cause) => new SshReadinessError({ @@ -953,7 +956,7 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio cause, }), ), - Effect.timeoutOption(Duration.millis(timeoutMs)), + Effect.timeoutOption(timeout), ); return yield* Option.match(result, { @@ -1236,7 +1239,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: yield* Effect.raceFirst( waitForHttpReady({ baseUrl: input.httpBaseUrl, - timeoutMs: SSH_READY_TIMEOUT_MS, + timeout: SSH_READY_TIMEOUT, }), exitFailure, ).pipe( @@ -1296,7 +1299,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: : child .kill({ killSignal: "SIGTERM", - forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT_MS, + forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT, }) .pipe(Effect.ignore), ), @@ -1540,7 +1543,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma [ tunnelEntry.process.kill({ killSignal: "SIGTERM", - forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT_MS, + forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT, }), stopRemoteServer( tunnelEntry.target, @@ -1594,7 +1597,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma remotePort: entry.remotePort, }); const readinessExit = yield* Effect.exit( - waitForHttpReady({ baseUrl: entry.httpBaseUrl, timeoutMs: 2_000 }), + waitForHttpReady({ baseUrl: entry.httpBaseUrl, timeout: REMOTE_REUSE_READY_TIMEOUT }), ); if (Exit.isSuccess(readinessExit)) { yield* Effect.logDebug("ssh.environment.tunnel.reused", { From ebbd80dee7d302ede3f928fe3071047dfc73d49b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 16:10:08 +0000 Subject: [PATCH 2/2] Normalize readiness duration inputs Co-authored-by: Julius Marminge --- packages/ssh/src/tunnel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 20c983b8497..54515adf926 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -877,9 +877,9 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio readonly probeTimeout?: Duration.Input; readonly path?: string; }): Effect.fn.Return { - const timeout = input.timeout ?? DEFAULT_HTTP_READY_TIMEOUT; - const interval = input.interval ?? HTTP_READY_RETRY_INTERVAL; - const probeTimeout = input.probeTimeout ?? SSH_READY_PROBE_TIMEOUT; + const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_HTTP_READY_TIMEOUT); + const interval = Duration.fromInputUnsafe(input.interval ?? HTTP_READY_RETRY_INTERVAL); + const probeTimeout = Duration.fromInputUnsafe(input.probeTimeout ?? SSH_READY_PROBE_TIMEOUT); const timeoutMs = Duration.toMillis(timeout); const intervalMs = Duration.toMillis(interval); const probeTimeoutMs = Duration.toMillis(probeTimeout);