From 333e34d708e6c3815af422c5bc982c390f4f874a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 29 Oct 2026 23:54:04 +0000 Subject: [PATCH 1/3] fix(install): Intel Macs get a clear message instead of a 404 --- scripts/install.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 4c164c45040f8..8f61779352799 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -133,6 +133,9 @@ case "$(uname -m)" in x86_64 | amd64) arch="x64" ;; *) fail "unsupported architecture $(uname -m)" ;; esac +if [ "${platform}-${arch}" = darwin-x64 ]; then + fail "there is no t3 CLI for Intel Macs. Install the desktop app from https://t3.codes/download, or run a server from source: https://github.com/${repo}/blob/main/docs/user/install.md#intel-macs" +fi command -v tar >/dev/null 2>&1 || fail "tar is required" if command -v sha256sum >/dev/null 2>&1; then checksum() { sha256sum "$1" | cut -d' ' -f1; } @@ -206,11 +209,11 @@ else elif [ "$fetch_status" -ne 0 ]; then fail "could not download the release checksums" fi + expected="$(grep " \*\{0,1\}${archive}\$" "${staging}/SHA256SUMS" | cut -d' ' -f1)" + [ -n "$expected" ] || fail "t3 ${version} has no release archive for ${platform}-${arch}" download "${base_url}/v${version}/${archive}" "${staging}/${archive}" step "Verifying the download..." - expected="$(grep " \*\{0,1\}${archive}\$" "${staging}/SHA256SUMS" | cut -d' ' -f1)" - [ -n "$expected" ] || fail "${archive} is not listed in SHA256SUMS" actual="$(checksum "${staging}/${archive}")" [ "$actual" = "$expected" ] || fail "checksum mismatch for ${archive}" From 5d27dda092eca582fd168a2e31b601c8cc48fc41 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 29 Oct 2026 23:54:06 +0000 Subject: [PATCH 2/3] fix(codex): broken Codex versions fail with an update hint instead of a decode error --- .../Adapters/CodexAdapterV2.test.ts | 53 +++++++++++++++++++ .../Adapters/CodexAdapterV2.ts | 29 ++++++++-- apps/server/src/provider/CodexInstallation.ts | 10 +++- .../src/provider/Drivers/CodexDriver.ts | 6 ++- .../provider-core/src/server/failure.test.ts | 14 ++++- packages/provider-core/src/server/failure.ts | 1 + 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 9dfe62bc4d36b..6413ed0bfe9ac 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -22,6 +22,7 @@ import { type ProviderApprovalDecision, ProviderInstanceId, ProviderSessionId, + ProviderSetupError, ProviderThreadId, ProviderTurnId, RunAttemptId, @@ -66,6 +67,8 @@ import * as Orchestrator from "../Orchestrator.ts"; import * as ProviderReplayHarness from "../testkit/ProviderReplayHarness.ts"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; +import * as ModelManifest from "../../provider/ModelManifest.ts"; +import { resolveProviderCompatibility } from "../../provider/providerCompatibility.ts"; import * as CodexAdapterV2 from "./CodexAdapterV2.ts"; import { withCodexReplayChildMetadata } from "./CodexAdapterV2.testkit.ts"; import * as CodexAdapterV2Testkit from "./CodexAdapterV2.testkit.ts"; @@ -876,6 +879,7 @@ describe("CodexAdapterV2 session initialize", () => { const openReplaySession = ( transcript: CodexReplay.CodexAppServerReplayTranscript, beforeEmitInbound?: CodexReplay.CodexAppServerReplayDriver["beforeEmitInbound"], + compatibility?: CodexAdapterV2.CodexAdapterV2Options["compatibility"], ) => Effect.gen(function* () { const driver = yield* CodexReplay.makeReplayDriver( @@ -887,6 +891,7 @@ describe("CodexAdapterV2 session initialize", () => { instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID, settings: DEFAULT_CODEX_SETTINGS, environment: {}, + ...(compatibility === undefined ? {} : { compatibility }), clientFactory: { open: (openInput) => Layer.build(CodexReplay.layerReplayWithDriver(driver)).pipe( @@ -1018,6 +1023,54 @@ describe("CodexAdapterV2 session initialize", () => { ), ); + it.effect("refuses to open a session on a Codex version marked broken", () => + Effect.gen(function* () { + const [initialize] = replayPreamble("broken-version"); + const failure = yield* openReplaySession( + makeCodexReplayTranscript({ + scenario: "broken-version", + entries: [ + initialize!, + { + type: "emit_inbound", + label: "initialize", + frame: { + id: 1, + result: { + userAgent: "T3 Code/0.144.3 (Mac OS 15.5.0; x86_64) unknown (T3 Code; 0.1.5)", + codexHome: "/tmp/codex-home", + platformFamily: "unix", + platformOs: "macos", + }, + }, + }, + ], + }), + undefined, + (version) => + Effect.succeed( + resolveProviderCompatibility( + ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, + CodexAdapterV2.CODEX_DRIVER_KIND, + version, + ), + ), + ).pipe(Effect.flip); + + assert.instanceOf(failure, ProviderAdapter.ProviderAdapterOpenSessionError); + assert.instanceOf(failure.cause, ProviderSetupError); + assert.equal( + failure.cause.message, + "Codex 0.144.3 does not work with this T3 Code release. Update Codex to >=0.159.0, then retry.", + ); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ); + it.effect("completes the handshake after a caller is interrupted mid-initialize", () => Effect.gen(function* () { const initializeAwaitingResponse = yield* Deferred.make(); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 4f960b6593c76..fff6362cabac8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -36,7 +36,8 @@ import { OrchestrationV2ProviderGoal, parseGoalCommand, ProviderDriverKind, - type ProviderSetupError, + ProviderSetupError, + type ServerProviderCompatibilityAdvisory, } from "@t3tools/contracts"; import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; import { AgentScope } from "@t3tools/shared/AgentScope"; @@ -1549,7 +1550,7 @@ export type CodexAdapterV2DriverEnv = export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* ( { instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput, - hooks: Pick = {}, + hooks: Pick = {}, ) { const clientFactory = yield* CodexAppServerClientFactory; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; @@ -1600,6 +1601,9 @@ export interface CodexAdapterV2Options { readonly clientFactory: CodexAppServerClientFactoryShape; readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"]; readonly resolveRuntime?: Effect.Effect; + readonly compatibility?: ( + version: string, + ) => Effect.Effect; readonly crypto: Crypto.Crypto; readonly fileSystem: FileSystem.FileSystem; readonly idAllocator: IdAllocator.IdAllocatorV2["Service"]; @@ -1706,24 +1710,41 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( return; } - yield* client + const userAgent = yield* client .request("initialize", { clientInfo: buildCodexInitializeParams().clientInfo, capabilities: CODEX_CLIENT_CAPABILITIES, }) .pipe( + Effect.map((response) => response.userAgent), Effect.catchTags({ // Interruption can leave Codex initialized before the local flag is set. CodexAppServerRequestError: (error) => error.code === -32600 && error.errorMessage === "Already initialized" - ? Effect.void + ? Effect.succeed(null) : Effect.fail(error), }), ); + const version = userAgent && /\/(\d+\.\d+\.\d+)/u.exec(userAgent)?.[1]; + const advisory = + version && adapterOptions.compatibility + ? yield* adapterOptions.compatibility(version) + : undefined; + if (advisory?.status === "broken") { + const target = + advisory.recommendedVersion ?? advisory.recommendedRange ?? "a newer version"; + return yield* new ProviderSetupError({ + instanceId: adapterOptions.instanceId, + operation: "runtime", + detail: `Codex ${version} does not work with this T3 Code release. Update Codex to ${target}, then retry.`, + }); + } yield* client.notify("initialized", undefined); yield* Ref.set(initialized, true); }), ); + // A failed open is discarded, so the next turn spawns the updated binary. + if (adapterOptions.compatibility !== undefined) yield* ensureInitialized; const now = yield* DateTime.now; const session = providerSession({ providerSessionId: input.providerSessionId, diff --git a/apps/server/src/provider/CodexInstallation.ts b/apps/server/src/provider/CodexInstallation.ts index 758eb406f1b54..88ca2c272711d 100644 --- a/apps/server/src/provider/CodexInstallation.ts +++ b/apps/server/src/provider/CodexInstallation.ts @@ -1,5 +1,9 @@ // @effect-diagnostics nodeBuiltinImport:off - Effect has no incremental digest. -import { ProviderDriverKind, type ProviderInstallState } from "@t3tools/contracts"; +import { + ProviderDriverKind, + type ProviderInstallState, + type ServerProviderCompatibilityAdvisory, +} from "@t3tools/contracts"; import { cliArchiveTarCommand } from "@t3tools/shared/cliRelease"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveCommandPath, resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -125,6 +129,9 @@ interface CodexInstallationService { readonly managedDirectory: string; readonly resolve: () => Effect.Effect; readonly acquire: () => Effect.Effect; + readonly compatibility: ( + version: string, + ) => Effect.Effect; readonly start: Effect.Effect; readonly cancel: ( operationId: string, @@ -796,6 +803,7 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function managedDirectory, resolve, acquire, + compatibility, start, cancel, state: SubscriptionRef.get(state), diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index bbd910cf5d4a9..97ec4954ed840 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -147,6 +147,7 @@ export const CodexDriver: ProviderDriver< const pathService = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; + const installation = yield* CodexInstallation.CodexInstallation; const modelCatalog = yield* ModelCatalog.ModelCatalog; const currentCatalog = modelCatalog.current(DRIVER_KIND); const processEnv = yield* mergeProviderInstanceEnvironment(environment); @@ -199,7 +200,10 @@ export const CodexDriver: ProviderDriver< enabled, config, }, - { onUsageLimits: (update) => snapshot.applyUsageLimits(update) }, + { + onUsageLimits: (update) => snapshot.applyUsageLimits(update), + compatibility: installation.compatibility, + }, ).pipe( Effect.mapError( (cause) => diff --git a/packages/provider-core/src/server/failure.test.ts b/packages/provider-core/src/server/failure.test.ts index a50d3dac9599c..b24f5bad04084 100644 --- a/packages/provider-core/src/server/failure.test.ts +++ b/packages/provider-core/src/server/failure.test.ts @@ -2,6 +2,8 @@ import { assert, it } from "@effect/vitest"; import { NodeId, ProviderDriverKind, + ProviderInstanceId, + ProviderSetupError, ProviderThreadId, ProviderTurnId, RunId, @@ -83,8 +85,16 @@ it("does not expose arbitrary cause messages and preserves explicit messages", ( } }); -it("preserves actionable handoff errors wrapped by turn startup", () => { - for (const error of [new ContextHandoffBudgetError(), new ContextHandoffBackgroundWorkError()]) { +it("preserves actionable T3 errors wrapped by turn startup", () => { + for (const error of [ + new ContextHandoffBudgetError(), + new ContextHandoffBackgroundWorkError(), + new ProviderSetupError({ + instanceId: ProviderInstanceId.make("codex"), + operation: "runtime", + detail: "Codex 0.144.3 does not work with this T3 Code release.", + }), + ]) { const cause = new ProviderAdapter.ProviderAdapterTurnStartError({ driver: ProviderDriverKind.make("codex"), threadId: ThreadId.make("thread:handoff-error"), diff --git a/packages/provider-core/src/server/failure.ts b/packages/provider-core/src/server/failure.ts index 2dd418ea2dfac..30cf077931cb0 100644 --- a/packages/provider-core/src/server/failure.ts +++ b/packages/provider-core/src/server/failure.ts @@ -108,6 +108,7 @@ function curatedCauseMessages(cause: unknown): { case "ClaudeSettingsLaunchArgError": case "ClaudeMcpRegistrationTimeoutError": case "CursorModelUnavailableError": + case "ProviderSetupError": return { known: stringField(cause, "message"), category: categoryMessage() }; case "ContextHandoffDeliveryUncertainError": return { From fd2de88b2e9228120d0fb69966de30d1155d54f9 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 30 Oct 2026 00:19:24 +0000 Subject: [PATCH 3/3] fix(codex): keep prerelease versions out of the broken-version gate --- apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index fff6362cabac8..1ebb5b5fb12d4 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1725,7 +1725,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : Effect.fail(error), }), ); - const version = userAgent && /\/(\d+\.\d+\.\d+)/u.exec(userAgent)?.[1]; + const version = userAgent && /\/(\S+)/u.exec(userAgent)?.[1]; const advisory = version && adapterOptions.compatibility ? yield* adapterOptions.compatibility(version)