From 810bdeeda0cf69b9577cb8e45dd96fe73c321fcd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 07:44:41 +0000 Subject: [PATCH 1/7] test(server): prune removes a runtime only its own entry names (red) Reproduces pingdotgg/t3code#25399: a runtime whose own directory is on PATH, or whose entry an earlier prune recorded in runtime/launchers, is pinned forever. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/pinnedRuntime.test.ts | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index 82230858891c4..4af75a87881bc 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -428,6 +428,41 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { }), ); + it.effect.each([ + { name: "its directory is on PATH", onPath: true, recorded: false }, + { name: "an earlier prune recorded its entry as a launcher", onPath: false, recorded: true }, + ])("removes a runtime only its own entry names when $name", ({ onPath, recorded }) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { baseDir, versionsDir } = yield* setup(["1.0.0", "1.1.0"]); + const binDir = path.join(baseDir, "bin"); + yield* fs.makeDirectory(binDir); + yield* fs.symlink(path.join(versionsDir, "1.1.0", "t3"), path.join(binDir, "t3")); + const ownDir = path.join(versionsDir, "1.0.0"); + if (recorded) { + yield* fs.writeFileString( + path.join(baseDir, "runtime", "launchers"), + `${path.join(ownDir, "t3")}\n`, + ); + } + + // The launch agent puts the running runtime's own directory on PATH. + const removed = yield* prunePinnedRuntimes({ baseDir, keep: ["2.0.0"] }).pipe( + Effect.provideService( + ProcessRunner.ProcessRunner, + lsofRunner("p1\nftxt\nn/sbin/launchd\n"), + ), + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provideService(HostProcess.Environment, { + T3CODE_INSTALL_BIN_DIR: binDir, + PATH: onPath ? `${ownDir}:/usr/bin:/bin` : "/usr/bin:/bin", + }), + ); + assert.deepEqual(removed, ["1.0.0"]); + }), + ); + it.effect("matches a Windows shim whatever its casing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; From 37dc44d3a264438e1fb31924bcd050dce46fc8d4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 07:48:45 +0000 Subject: [PATCH 2/7] fix(server): runtime prune no longer keeps a version whose directory is on PATH The macOS launch agent puts a runtime version directory on the service PATH, so prune took that runtime's own entry binary for a `t3` launcher, recorded it in runtime/launchers, and never removed the version. A candidate launcher whose own path lies inside the versions directory, through the T3 home as given or its real path, is now skipped whether it came from PATH or the launchers file. Launchers outside it that resolve into it are unchanged. Fixes pingdotgg/t3code#25398 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/pinnedRuntime.test.ts | 24 +++++++++++++++++---- apps/server/src/cloud/pinnedRuntime.ts | 12 +++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index 4af75a87881bc..6a9d20e071930 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -429,9 +429,20 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { ); it.effect.each([ - { name: "its directory is on PATH", onPath: true, recorded: false }, - { name: "an earlier prune recorded its entry as a launcher", onPath: false, recorded: true }, - ])("removes a runtime only its own entry names when $name", ({ onPath, recorded }) => + { name: "its directory is on PATH", onPath: true, recorded: false, linkedHome: false }, + { + name: "an earlier prune recorded its entry as a launcher", + onPath: false, + recorded: true, + linkedHome: false, + }, + { + name: "its real directory in a symlinked home is on PATH", + onPath: true, + recorded: false, + linkedHome: true, + }, + ])("removes a runtime only its own entry names when $name", ({ onPath, recorded, linkedHome }) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -447,8 +458,13 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { ); } + const home = linkedHome + ? path.join(yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-link-" }), "home") + : baseDir; + if (linkedHome) yield* fs.symlink(baseDir, home); + // The launch agent puts the running runtime's own directory on PATH. - const removed = yield* prunePinnedRuntimes({ baseDir, keep: ["2.0.0"] }).pipe( + const removed = yield* prunePinnedRuntimes({ baseDir: home, keep: ["2.0.0"] }).pipe( Effect.provideService( ProcessRunner.ProcessRunner, lsofRunner("p1\nftxt\nn/sbin/launchd\n"), diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index e8db9892634d3..743211cc7ea85 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -413,9 +413,14 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ const realVersionsDir = yield* fs .realPath(versionsDir) .pipe(Effect.orElseSucceed(() => versionsDir)); + const inVersionsDir = (candidate: string) => + launcherOwnsVersionsDir(path, versionsDir, candidate) || + launcherOwnsVersionsDir(path, realVersionsDir, candidate); const unrecorded: Array = []; const targets: Array = []; for (const launcher of [...recorded, ...found]) { + // A runtime's own entry, reached through a version directory on the service PATH. + if (inVersionsDir(launcher)) continue; const target = platform === "win32" ? yield* fs.readFileString(launcher).pipe( @@ -426,12 +431,7 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ yield* fs.realPath(launcher).pipe(Effect.orElseSucceed(() => undefined)); if (target === undefined) continue; targets.push(target); - if ( - (launcherOwnsVersionsDir(path, versionsDir, target) || - launcherOwnsVersionsDir(path, realVersionsDir, target)) && - !recorded.includes(launcher) && - !unrecorded.includes(launcher) - ) { + if (inVersionsDir(target) && !recorded.includes(launcher) && !unrecorded.includes(launcher)) { unrecorded.push(launcher); } } From 6dc299399f39171a67f2a19024d7533de00ac92c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 08:55:26 +0000 Subject: [PATCH 3/7] fix(server): start-up prune keeps the runtime a stopped service's unit starts The PATH scan used to record a macOS service's launcher runtime by accident, which was the only thing keeping it once in-place updates moved the active version. Read the installed unit, as t3 update already does, so an unmanaged start cannot prune it. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.ts | 33 ++++++++++++++---------- apps/server/src/cloud/pinnedRuntime.ts | 2 +- apps/server/src/cloud/selfUpdate.test.ts | 24 +++++++++++++++++ apps/server/src/cloud/selfUpdate.ts | 12 +++++++++ 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 15b3b16a7bc0f..c9d48e9a9e6f8 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -203,17 +203,29 @@ export interface BootServiceManager { readonly finalize: ReadonlyArray; } +const systemdUnitPath = (path: Path.Path, homeDir: string) => + path.join(homeDir, ".config", "systemd", "user", BOOT_SERVICE_UNIT_FILE); + +const launchdPlistPath = (path: Path.Path, homeDir: string) => + path.join(homeDir, "Library", "LaunchAgents", BOOT_SERVICE_PLIST_FILE); + +/** Where this platform's service manager keeps the unit, whether or not one is installed. */ +export function bootServiceUnitPath(input: { + readonly path: Path.Path; + readonly platform: NodeJS.Platform; + readonly homeDir: string; +}): string | undefined { + if (input.homeDir === "") return undefined; + if (input.platform === "linux") return systemdUnitPath(input.path, input.homeDir); + if (input.platform === "darwin") return launchdPlistPath(input.path, input.homeDir); + return undefined; +} + function systemdManager(input: { readonly path: Path.Path; readonly homeDir: string; }): BootServiceManager { - const unitPath = input.path.join( - input.homeDir, - ".config", - "systemd", - "user", - BOOT_SERVICE_UNIT_FILE, - ); + const unitPath = systemdUnitPath(input.path, input.homeDir); const prepareActivation = [ { step: "reloading systemd user units", @@ -285,12 +297,7 @@ function launchdManager(input: { readonly uid: number; readonly environmentPath: string; }): BootServiceManager { - const unitPath = input.path.join( - input.homeDir, - "Library", - "LaunchAgents", - BOOT_SERVICE_PLIST_FILE, - ); + const unitPath = launchdPlistPath(input.path, input.homeDir); const domainTarget = `gui/${input.uid}`; const serviceTarget = `${domainTarget}/${BOOT_SERVICE_LAUNCHD_LABEL}`; // Unloaded jobs may reject bootout/enable. Bootstrap starts RunAtLoad/KeepAlive jobs; kickstart would restart them. diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index 743211cc7ea85..422f8ab7a23be 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -419,7 +419,7 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ const unrecorded: Array = []; const targets: Array = []; for (const launcher of [...recorded, ...found]) { - // A runtime's own entry, reached through a version directory on the service PATH. + // A runtime's own entry, on the macOS service PATH or recorded by an earlier prune, is no launcher. if (inVersionsDir(launcher)) continue; const target = platform === "win32" diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index b359726aafa23..8853cf5e4d52c 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -407,6 +407,7 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { readonly executable: "own runtime" | "elsewhere"; readonly managed?: boolean; readonly symlinkedHome?: boolean; + readonly servicePlistStarts?: string; }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -424,6 +425,16 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { yield* fs.utimes(sentinel, 0, 0); } if (input.symlinkedHome) yield* fs.symlink(realHome, baseDir); + const userHome = path.join(tempDir, "user"); + if (input.servicePlistStarts !== undefined) { + const agentsDir = path.join(userHome, "Library", "LaunchAgents"); + yield* fs.makeDirectory(agentsDir, { recursive: true }); + const program = path.join(versionsDir, input.servicePlistStarts, "t3"); + yield* fs.writeFileString( + path.join(agentsDir, "com.t3tools.t3code.service.plist"), + `\nProgramArguments\n\n${program}\n__service-launcher\n\n\n`, + ); + } // Like `process.execPath`, the running runtime is reported by its real path. const executablePath = input.executable === "own runtime" @@ -436,6 +447,7 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { Effect.provideService(HostProcess.ExecutablePath, executablePath), Effect.provideService(HostProcess.Platform, "darwin"), Effect.provideService(HostProcess.Environment, {}), + Effect.provideService(HostProcess.HomeDirectory, userHome), Effect.provideService(ProcessRunner.ProcessRunner, otherProcesses), ); return { removed, remaining: (yield* fs.readDirectory(versionsDir)).toSorted() }; @@ -464,6 +476,18 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { }), ); + // In-place updates move the active version but leave the launch agent on the launcher's runtime. + it.effect("keeps the runtime a stopped service's launch agent starts", () => + Effect.gen(function* () { + const { removed, remaining } = yield* pruneOnStart({ + executable: "own runtime", + servicePlistStarts: staleVersion, + }); + expect(removed).toEqual([]); + expect(remaining).toEqual([staleVersion, packageJson.version].toSorted()); + }), + ); + it.effect("leaves runtimes alone for an unmanaged server started elsewhere", () => Effect.gen(function* () { const { removed, remaining } = yield* pruneOnStart({ executable: "elsewhere" }); diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index dca833d8e5bda..6cee5b5b863db 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -37,6 +37,7 @@ import { PinnedRuntimePreflightBlockedError, prunePinnedRuntimes, } from "./pinnedRuntime.ts"; +import { bootServiceProgramOf, bootServiceUnitPath } from "./bootService.ts"; import { decodeServicePreflightResult } from "./servicePreflight.ts"; import * as ServiceLauncherClient from "./serviceLauncherClient.ts"; import { isExactServiceVersion, SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts"; @@ -191,9 +192,20 @@ export const pruneRuntimesOnStart = Effect.fn("cloud.server_self_update.prune_on if (!input.managed && executablePath !== entryPath && executablePath !== realEntryPath) { return []; } + const unitPath = bootServiceUnitPath({ + path, + platform, + homeDir: yield* HostProcess.HomeDirectory, + }); + // A stopped service's unit still names the launcher runtime it will start. + const unit = + unitPath === undefined + ? Option.none() + : yield* fs.readFileString(unitPath).pipe(Effect.option); const removed = yield* prunePinnedRuntimes({ baseDir: input.baseDir, keep: [packageJson.version], + references: Option.toArray(Option.flatMapNullishOr(unit, bootServiceProgramOf)), }); if (removed.length > 0) { yield* Effect.logInfo("Removed unused t3 runtimes.", { versions: removed }); From 11b545c0c1dff95fa95c68b56aef58e085c1b0d1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 09:29:21 +0000 Subject: [PATCH 4/7] fix(server): prune skips only a runtime's own entry, not launchers under versions/ A launcher may live in a custom bin directory under the versions directory. Skip a candidate only when it resolves to itself inside a version directory, which is what a runtime's entry binary does and a launcher symlink never does. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/pinnedRuntime.test.ts | 26 +++++++++++++++++++++ apps/server/src/cloud/pinnedRuntime.ts | 10 ++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index 6a9d20e071930..76dc5dee00781 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -479,6 +479,32 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { }), ); + it.effect("keeps a runtime a launcher inside the versions directory names", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { baseDir, versionsDir } = yield* setup(["1.0.0", "1.1.0"]); + // The installer accepts any bin directory, including one under the versions directory. + const binDir = path.join(versionsDir, "bin"); + yield* fs.makeDirectory(binDir); + yield* fs.symlink(path.join(versionsDir, "1.0.0", "t3"), path.join(binDir, "t3")); + yield* fs.writeFileString( + path.join(baseDir, "runtime", "launchers"), + `${path.join(binDir, "t3")}\n`, + ); + + const removed = yield* prunePinnedRuntimes({ baseDir, keep: ["2.0.0"] }).pipe( + Effect.provideService( + ProcessRunner.ProcessRunner, + lsofRunner("p1\nftxt\nn/sbin/launchd\n"), + ), + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provideService(HostProcess.Environment, { PATH: "/usr/bin:/bin" }), + ); + assert.deepEqual(removed, ["1.1.0"]); + }), + ); + it.effect("matches a Windows shim whatever its casing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index 422f8ab7a23be..a25c27ea2a960 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -419,8 +419,6 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ const unrecorded: Array = []; const targets: Array = []; for (const launcher of [...recorded, ...found]) { - // A runtime's own entry, on the macOS service PATH or recorded by an earlier prune, is no launcher. - if (inVersionsDir(launcher)) continue; const target = platform === "win32" ? yield* fs.readFileString(launcher).pipe( @@ -430,6 +428,14 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ : // Fully resolved, through chained links and a symlinked T3 home alike. yield* fs.realPath(launcher).pipe(Effect.orElseSucceed(() => undefined)); if (target === undefined) continue; + // A runtime's own entry, on the macOS service PATH or recorded by an earlier prune, is no launcher. + if (platform !== "win32" && inVersionsDir(target)) { + const unlinked = yield* fs.realPath(path.dirname(launcher)).pipe( + Effect.map((directory) => path.join(directory, path.basename(launcher))), + Effect.orElseSucceed(() => undefined), + ); + if (unlinked === target) continue; + } targets.push(target); if (inVersionsDir(target) && !recorded.includes(launcher) && !unrecorded.includes(launcher)) { unrecorded.push(launcher); From d607be310dae34783b110bd5f5fc545d8ab5c2a2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 09:42:15 +0000 Subject: [PATCH 5/7] fix(server): start-up prune matches a service unit that names the runtime through a linked home Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/selfUpdate.test.ts | 13 +++++++++++-- apps/server/src/cloud/selfUpdate.ts | 10 +++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index 8853cf5e4d52c..beae0f3e85843 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -408,6 +408,7 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { readonly managed?: boolean; readonly symlinkedHome?: boolean; readonly servicePlistStarts?: string; + readonly servicePlistThroughLink?: boolean; }) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -429,7 +430,11 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { if (input.servicePlistStarts !== undefined) { const agentsDir = path.join(userHome, "Library", "LaunchAgents"); yield* fs.makeDirectory(agentsDir, { recursive: true }); - const program = path.join(versionsDir, input.servicePlistStarts, "t3"); + const programHome = input.servicePlistThroughLink + ? path.join(tempDir, "alias-home") + : baseDir; + if (input.servicePlistThroughLink) yield* fs.symlink(realHome, programHome); + const program = path.join(programHome, "runtime", "versions", input.servicePlistStarts, "t3"); yield* fs.writeFileString( path.join(agentsDir, "com.t3tools.t3code.service.plist"), `\nProgramArguments\n\n${program}\n__service-launcher\n\n\n`, @@ -477,11 +482,15 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { ); // In-place updates move the active version but leave the launch agent on the launcher's runtime. - it.effect("keeps the runtime a stopped service's launch agent starts", () => + it.effect.each([ + { name: "", throughLink: false }, + { name: " through a symlinked T3 home", throughLink: true }, + ])("keeps the runtime a stopped service's launch agent starts$name", ({ throughLink }) => Effect.gen(function* () { const { removed, remaining } = yield* pruneOnStart({ executable: "own runtime", servicePlistStarts: staleVersion, + servicePlistThroughLink: throughLink, }); expect(removed).toEqual([]); expect(remaining).toEqual([staleVersion, packageJson.version].toSorted()); diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index 6cee5b5b863db..6709ecbff1032 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -202,10 +202,18 @@ export const pruneRuntimesOnStart = Effect.fn("cloud.server_self_update.prune_on unitPath === undefined ? Option.none() : yield* fs.readFileString(unitPath).pipe(Effect.option); + const program = Option.flatMapNullishOr(unit, bootServiceProgramOf); + // The unit may name it through another spelling of a symlinked T3 home. + const references = Option.isNone(program) + ? [] + : [ + program.value, + yield* fs.realPath(program.value).pipe(Effect.orElseSucceed(() => program.value)), + ]; const removed = yield* prunePinnedRuntimes({ baseDir: input.baseDir, keep: [packageJson.version], - references: Option.toArray(Option.flatMapNullishOr(unit, bootServiceProgramOf)), + references, }); if (removed.length > 0) { yield* Effect.logInfo("Removed unused t3 runtimes.", { versions: removed }); From 99da2d3315712a84955417d4c63005d6fe09898b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 10:05:13 +0000 Subject: [PATCH 6/7] fix(server): a PATH directory linked to a version still pins it Only a candidate written as versions//t3 that is not itself a link is a runtime's own entry; a linked PATH directory reaches the same file under another name. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/pinnedRuntime.test.ts | 30 ++++++++++++++------- apps/server/src/cloud/pinnedRuntime.ts | 4 ++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index 76dc5dee00781..2feb8b4199b99 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -479,19 +479,29 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { }), ); - it.effect("keeps a runtime a launcher inside the versions directory names", () => + it.effect.each([ + { name: "a launcher inside the versions directory", via: "bin" as const }, + { name: "a PATH directory linked to its version directory", via: "linked dir" as const }, + ])("keeps a runtime $name names", ({ via }) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const { baseDir, versionsDir } = yield* setup(["1.0.0", "1.1.0"]); - // The installer accepts any bin directory, including one under the versions directory. - const binDir = path.join(versionsDir, "bin"); - yield* fs.makeDirectory(binDir); - yield* fs.symlink(path.join(versionsDir, "1.0.0", "t3"), path.join(binDir, "t3")); - yield* fs.writeFileString( - path.join(baseDir, "runtime", "launchers"), - `${path.join(binDir, "t3")}\n`, - ); + let pathDirs = "/usr/bin:/bin"; + if (via === "bin") { + // The installer accepts any bin directory, including one under the versions directory. + const binDir = path.join(versionsDir, "bin"); + yield* fs.makeDirectory(binDir); + yield* fs.symlink(path.join(versionsDir, "1.0.0", "t3"), path.join(binDir, "t3")); + yield* fs.writeFileString( + path.join(baseDir, "runtime", "launchers"), + `${path.join(binDir, "t3")}\n`, + ); + } else { + const linkedDir = path.join(baseDir, "current"); + yield* fs.symlink(path.join(versionsDir, "1.0.0"), linkedDir); + pathDirs = `${linkedDir}:${pathDirs}`; + } const removed = yield* prunePinnedRuntimes({ baseDir, keep: ["2.0.0"] }).pipe( Effect.provideService( @@ -499,7 +509,7 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { lsofRunner("p1\nftxt\nn/sbin/launchd\n"), ), Effect.provideService(HostProcess.Platform, "darwin"), - Effect.provideService(HostProcess.Environment, { PATH: "/usr/bin:/bin" }), + Effect.provideService(HostProcess.Environment, { PATH: pathDirs }), ); assert.deepEqual(removed, ["1.1.0"]); }), diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index a25c27ea2a960..4006ed70a3799 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -429,7 +429,9 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ yield* fs.realPath(launcher).pipe(Effect.orElseSucceed(() => undefined)); if (target === undefined) continue; // A runtime's own entry, on the macOS service PATH or recorded by an earlier prune, is no launcher. - if (platform !== "win32" && inVersionsDir(target)) { + // It is named as `versions//t3` and is not itself a link. + const parent = path.dirname(path.dirname(path.resolve(launcher))); + if (platform !== "win32" && (parent === versionsDir || parent === realVersionsDir)) { const unlinked = yield* fs.realPath(path.dirname(launcher)).pipe( Effect.map((directory) => path.join(directory, path.basename(launcher))), Effect.orElseSucceed(() => undefined), From 47f02e52881e77f403c7dd0cff8ab867e2b5dd5f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:21:35 +0000 Subject: [PATCH 7/7] fix(server): a linked directory inside versions/ still pins its runtime A PATH directory such as versions/bin -> versions/1.0.0 was taken for 1.0.0's own directory, so prune dropped the launcher and deleted the runtime it starts. An entry now counts as a runtime's own only when it resolves to itself under the real versions directory. AI-assisted; verified with the tests and tools listed above. --- apps/server/src/cloud/pinnedRuntime.test.ts | 6 +++++- apps/server/src/cloud/pinnedRuntime.ts | 17 +++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index d5d8639e32e2f..68870bd969436 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -565,6 +565,10 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { it.effect.each([ { name: "a launcher inside the versions directory", via: "bin" as const }, { name: "a PATH directory linked to its version directory", via: "linked dir" as const }, + { + name: "a PATH directory in the versions directory linked to its version directory", + via: "linked versions dir" as const, + }, ])("keeps a runtime $name names", ({ via }) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -581,7 +585,7 @@ it.layer(NodeServices.layer)("prunePinnedRuntimes", (it) => { `${path.join(binDir, "t3")}\n`, ); } else { - const linkedDir = path.join(baseDir, "current"); + const linkedDir = path.join(via === "linked dir" ? baseDir : versionsDir, "current"); yield* fs.symlink(path.join(versionsDir, "1.0.0"), linkedDir); pathDirs = `${linkedDir}:${pathDirs}`; } diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index 804b590101217..77bcb39f09dcf 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -510,14 +510,15 @@ const shellLauncherTargets = Effect.fn("cloud.pinned_runtime.shell_launcher_targ yield* fs.realPath(launcher).pipe(Effect.orElseSucceed(() => undefined)); if (target === undefined) continue; // A runtime's own entry, on the macOS service PATH or recorded by an earlier prune, is no launcher. - // It is named as `versions//t3` and is not itself a link. - const parent = path.dirname(path.dirname(path.resolve(launcher))); - if (platform !== "win32" && (parent === versionsDir || parent === realVersionsDir)) { - const unlinked = yield* fs.realPath(path.dirname(launcher)).pipe( - Effect.map((directory) => path.join(directory, path.basename(launcher))), - Effect.orElseSucceed(() => undefined), - ); - if (unlinked === target) continue; + // It is named as `versions//t3`, and neither it nor its directory is a link. + const entryDir = path.dirname(path.resolve(launcher)); + const parent = path.dirname(entryDir); + if ( + platform !== "win32" && + (parent === versionsDir || parent === realVersionsDir) && + target === path.join(realVersionsDir, path.basename(entryDir), path.basename(launcher)) + ) { + continue; } targets.push(target); if (inVersionsDir(target) && !recorded.includes(launcher) && !unrecorded.includes(launcher)) {