Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type ProviderApprovalDecision,
ProviderInstanceId,
ProviderSessionId,
ProviderSetupError,
ProviderThreadId,
ProviderTurnId,
RunAttemptId,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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<void>();
Expand Down
29 changes: 25 additions & 4 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1549,7 +1550,7 @@ export type CodexAdapterV2DriverEnv =

export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* (
{ instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput<CodexSettings>,
hooks: Pick<CodexAdapterV2Options, "onUsageLimits" | "resolveRuntime"> = {},
hooks: Pick<CodexAdapterV2Options, "onUsageLimits" | "resolveRuntime" | "compatibility"> = {},
) {
const clientFactory = yield* CodexAppServerClientFactory;
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
Expand Down Expand Up @@ -1600,6 +1601,9 @@ export interface CodexAdapterV2Options {
readonly clientFactory: CodexAppServerClientFactoryShape;
readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"];
readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>;
readonly compatibility?: (
version: string,
) => Effect.Effect<ServerProviderCompatibilityAdvisory | undefined>;
readonly crypto: Crypto.Crypto;
readonly fileSystem: FileSystem.FileSystem;
readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
Expand Down Expand Up @@ -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 && /\/(\S+)/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,
Expand Down
10 changes: 9 additions & 1 deletion apps/server/src/provider/CodexInstallation.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -125,6 +129,9 @@ interface CodexInstallationService {
readonly managedDirectory: string;
readonly resolve: () => Effect.Effect<CodexExecutable, CodexInstallationError>;
readonly acquire: () => Effect.Effect<CodexExecutable, CodexInstallationError, Scope.Scope>;
readonly compatibility: (
version: string,
) => Effect.Effect<ServerProviderCompatibilityAdvisory | undefined>;
readonly start: Effect.Effect<ProviderInstallState, CodexInstallationError>;
readonly cancel: (
operationId: string,
Expand Down Expand Up @@ -796,6 +803,7 @@ export const makeCodexInstallation = Effect.fn("makeCodexInstallation")(function
managedDirectory,
resolve,
acquire,
compatibility,
start,
cancel,
state: SubscriptionRef.get(state),
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) =>
Expand Down
14 changes: 12 additions & 2 deletions packages/provider-core/src/server/failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assert, it } from "@effect/vitest";
import {
NodeId,
ProviderDriverKind,
ProviderInstanceId,
ProviderSetupError,
ProviderThreadId,
ProviderTurnId,
RunId,
Expand Down Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions packages/provider-core/src/server/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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}"

Expand Down
Loading