Skip to content

Commit 6e06919

Browse files
authored
sync: port upstream Codex/Claude/Grok ACP fixes; adapt Cursor SDK (pingdotgg#3948, pingdotgg#2124, pingdotgg#3932, pingdotgg#4094) (#167)
## What changed - `pingdotgg#3948` include runtime model and effort in Codex developer instructions - `pingdotgg#2124` thread cwd through Claude capability probe (kept fork `CLAUDE_CONFIG_DIR` semantics) - `pingdotgg#3932` prevent ACP assistant ID collisions after restarts (Grok + shared `AcpSessionRuntime`; skipped deleted Cursor ACP files) - `pingdotgg#4094` default legacy Cursor binary path to `cursor-agent` without restoring ACP — Cursor remains on `@cursor/sdk` Preserves fork: Cursor SDK migration, Grok ACP, multi-provider management. ## Validation - `vp check` / `vp run typecheck` on stack tip - CI: pending Stack: 2 of 5; depends on #166.
2 parents dcd00ef + 4647361 commit 6e06919

18 files changed

Lines changed: 200 additions & 29 deletions

apps/server/src/provider/CodexDeveloperInstructions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ProviderInteractionMode } from "@t3tools/contracts";
2+
13
const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `
24
35
## T3 Code collaborative browser
@@ -145,3 +147,26 @@ The \`request_user_input\` tool is unavailable in Default mode. If you call it w
145147
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
146148
${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}
147149
</collaboration_mode>`;
150+
151+
export interface CodexRuntimeInfo {
152+
readonly model: string;
153+
readonly reasoningEffort: string;
154+
}
155+
156+
// Values come from trusted config, but keep the block single-line regardless.
157+
function toSingleLine(value: string): string {
158+
return value.replaceAll(/\s+/g, " ").trim();
159+
}
160+
161+
export function buildCodexDeveloperInstructions(
162+
interactionMode: ProviderInteractionMode,
163+
runtime: CodexRuntimeInfo,
164+
): string {
165+
const base =
166+
interactionMode === "plan"
167+
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
168+
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
169+
return `${base}
170+
171+
<runtime_info>In case you're asked: you are running in T3 Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`;
172+
}

apps/server/src/provider/Drivers/ClaudeDriver.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
119119
Effect.gen(function* () {
120120
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
121121
const path = yield* Path.Path;
122+
const { cwd } = yield* ServerConfig;
122123
const httpClient = yield* HttpClient.HttpClient;
123124
const serverSettings = yield* ServerSettingsService;
124125
const eventLoggers = yield* ProviderEventLoggers;
@@ -154,11 +155,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
154155
capacity: 1,
155156
timeToLive: CAPABILITIES_PROBE_TTL,
156157
lookup: () =>
157-
probeClaudeCapabilities(effectiveConfig, processEnv).pipe(
158+
probeClaudeCapabilities(effectiveConfig, processEnv, cwd).pipe(
158159
Effect.provideService(Path.Path, path),
159160
),
160161
});
161-
const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig);
162+
const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);
162163

163164
const checkProvider = checkClaudeProviderStatus(
164165
effectiveConfig,

apps/server/src/provider/Drivers/ClaudeHome.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,20 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
4141
`claude:home:${configDir}`,
4242
);
4343
expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe(
44-
`claude\0${configDir}`,
44+
`claude\0${configDir}\0`,
4545
);
4646
}),
4747
);
4848

49+
it.effect("separates capability probes by cwd", () =>
50+
Effect.gen(function* () {
51+
const config = { binaryPath: "claude", homePath: "" };
52+
const first = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-a");
53+
const second = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-b");
54+
expect(first).not.toBe(second);
55+
}),
56+
);
57+
4958
it.effect("treats paths that already end with .claude as the config dir", () =>
5059
Effect.gen(function* () {
5160
const path = yield* Path.Path;

apps/server/src/provider/Drivers/ClaudeHome.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationG
7272
export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesCacheKey")(
7373
function* (
7474
config: Pick<ClaudeSettings, "binaryPath" | "homePath">,
75+
cwd?: string,
7576
): Effect.fn.Return<string, never, Path.Path> {
7677
const configDir = yield* resolveClaudeConfigDir(config);
77-
return `${config.binaryPath}\0${configDir}`;
78+
return `${config.binaryPath}\0${configDir}\0${cwd ?? ""}`;
7879
},
7980
);

apps/server/src/provider/Drivers/GrokDriver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
8484
defaultConfig: (): GrokSettings => decodeGrokSettings({}),
8585
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
8686
Effect.gen(function* () {
87+
const crypto = yield* Crypto.Crypto;
8788
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
8889
const httpClient = yield* HttpClient.HttpClient;
8990
const serverSettings = yield* ServerSettingsService;
@@ -114,6 +115,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
114115

115116
const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe(
116117
Effect.map(stampIdentity),
118+
Effect.provideService(Crypto.Crypto, crypto),
117119
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
118120
);
119121

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ function waitForAbortSignal(signal: AbortSignal): Promise<void> {
644644
const probeClaudeCapabilities = (
645645
claudeSettings: ClaudeSettings,
646646
environment?: NodeJS.ProcessEnv,
647+
cwd?: string,
647648
) => {
648649
const abort = new AbortController();
649650
return Effect.gen(function* () {
@@ -663,6 +664,7 @@ const probeClaudeCapabilities = (
663664
settingSources: ["user", "project", "local"],
664665
allowedTools: [],
665666
env: claudeEnvironment,
667+
...(cwd ? { cwd } : {}),
666668
stderr: () => {},
667669
},
668670
});

apps/server/src/provider/Layers/CodexSessionRuntime.test.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { it } from "@effect/vitest";
44
import * as Effect from "effect/Effect";
55
import * as Schema from "effect/Schema";
66
import { describe } from "vite-plus/test";
7-
import { ThreadId } from "@t3tools/contracts";
7+
import { DEFAULT_MODEL, ThreadId } from "@t3tools/contracts";
88
import * as CodexErrors from "effect-codex-app-server/errors";
99
import * as CodexRpc from "effect-codex-app-server/rpc";
1010

1111
import {
12+
buildCodexDeveloperInstructions,
1213
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
1314
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
1415
} from "../CodexDeveloperInstructions.ts";
@@ -118,7 +119,10 @@ describe("buildTurnStartParams", () => {
118119
settings: {
119120
model: "gpt-5.3-codex",
120121
reasoning_effort: "medium",
121-
developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
122+
developer_instructions: buildCodexDeveloperInstructions("plan", {
123+
model: "gpt-5.3-codex",
124+
reasoningEffort: "medium",
125+
}),
122126
},
123127
},
124128
});
@@ -163,12 +167,31 @@ describe("buildTurnStartParams", () => {
163167
settings: {
164168
model: "gpt-5.3-codex",
165169
reasoning_effort: "medium",
166-
developer_instructions: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
170+
developer_instructions: buildCodexDeveloperInstructions("default", {
171+
model: "gpt-5.3-codex",
172+
reasoningEffort: "medium",
173+
}),
167174
},
168175
},
169176
});
170177
});
171178

179+
it("reports the same fallback model and effort in settings and instructions", () => {
180+
const params = Effect.runSync(
181+
buildTurnStartParams({
182+
threadId: "provider-thread-1",
183+
runtimeMode: "full-access",
184+
prompt: "Go",
185+
interactionMode: "default",
186+
}),
187+
);
188+
189+
const settings = params.collaborationMode?.settings;
190+
NodeAssert.equal(settings?.model, DEFAULT_MODEL);
191+
NodeAssert.equal(settings?.reasoning_effort, "medium");
192+
NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));
193+
});
194+
172195
it("omits collaboration mode when interaction mode is absent", () => {
173196
const params = Effect.runSync(
174197
buildTurnStartParams({
@@ -194,6 +217,53 @@ describe("buildTurnStartParams", () => {
194217
});
195218
});
196219

220+
describe("buildCodexDeveloperInstructions", () => {
221+
it("appends runtime info after the mode instructions", () => {
222+
const instructions = buildCodexDeveloperInstructions("default", {
223+
model: "gpt-5.3-codex",
224+
reasoningEffort: "high",
225+
});
226+
227+
NodeAssert.ok(instructions.startsWith(CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS));
228+
NodeAssert.match(instructions, /T3 Code/);
229+
NodeAssert.match(instructions, /Codex harness/);
230+
NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/);
231+
});
232+
233+
it("includes runtime info alongside plan mode instructions", () => {
234+
const instructions = buildCodexDeveloperInstructions("plan", {
235+
model: "gpt-5.3-codex",
236+
reasoningEffort: "medium",
237+
});
238+
239+
NodeAssert.ok(instructions.startsWith(CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS));
240+
NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/);
241+
});
242+
243+
it("varies with the model and effort of each turn", () => {
244+
const first = buildCodexDeveloperInstructions("default", {
245+
model: "gpt-5.3-codex",
246+
reasoningEffort: "medium",
247+
});
248+
const second = buildCodexDeveloperInstructions("default", {
249+
model: "gpt-5.4",
250+
reasoningEffort: "high",
251+
});
252+
253+
NodeAssert.notEqual(first, second);
254+
});
255+
256+
it("flattens multiline metadata into single-line runtime info", () => {
257+
const instructions = buildCodexDeveloperInstructions("default", {
258+
model: "gpt\n5.3\ncodex",
259+
reasoningEffort: " high\neffort ",
260+
});
261+
262+
NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/);
263+
NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/);
264+
});
265+
});
266+
197267
describe("T3 browser developer instructions", () => {
198268
it("prefers the product-native preview tools in both collaboration modes", () => {
199269
for (const instructions of [

apps/server/src/provider/Layers/CodexSessionRuntime.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,7 @@ import * as EffectCodexSchema from "effect-codex-app-server/schema";
3737

3838
import { buildCodexInitializeParams } from "./CodexProvider.ts";
3939
import { expandHomePath } from "../../pathExpansion.ts";
40-
import {
41-
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
42-
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
43-
} from "../CodexDeveloperInstructions.ts";
40+
import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts";
4441
const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);
4542

4643
const PROVIDER = ProviderDriverKind.make("codex");
@@ -333,15 +330,16 @@ function buildCodexCollaborationMode(input: {
333330
return undefined;
334331
}
335332
const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL;
333+
const reasoningEffort = input.effort ?? "medium";
336334
return {
337335
mode: input.interactionMode,
338336
settings: {
339337
model,
340-
reasoning_effort: input.effort ?? "medium",
341-
developer_instructions:
342-
input.interactionMode === "plan"
343-
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
344-
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
338+
reasoning_effort: reasoningEffort,
339+
developer_instructions: buildCodexDeveloperInstructions(input.interactionMode, {
340+
model,
341+
reasoningEffort,
342+
}),
345343
},
346344
};
347345
}

apps/server/src/provider/Layers/GrokAdapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
596596
: {}),
597597
...acpNativeLoggers,
598598
}).pipe(
599+
Effect.provideService(Crypto.Crypto, crypto),
599600
Effect.provideService(Scope.Scope, sessionScope),
600601
Effect.mapError(
601602
(cause) =>

apps/server/src/provider/Layers/GrokProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@t3tools/contracts";
88
import type * as EffectAcpSchema from "effect-acp/schema";
99
import { causeErrorTag } from "@t3tools/shared/observability";
10+
import * as Crypto from "effect/Crypto";
1011
import * as DateTime from "effect/DateTime";
1112
import * as Effect from "effect/Effect";
1213
import * as Exit from "effect/Exit";
@@ -167,7 +168,11 @@ const runGrokVersionCommand = (
167168
export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
168169
grokSettings: GrokSettings,
169170
environment: NodeJS.ProcessEnv = process.env,
170-
): Effect.fn.Return<ServerProviderDraft, never, ChildProcessSpawner.ChildProcessSpawner> {
171+
): Effect.fn.Return<
172+
ServerProviderDraft,
173+
never,
174+
ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto
175+
> {
171176
const checkedAt = DateTime.formatIso(yield* DateTime.now);
172177
const fallbackModels = grokModelsFromSettings(grokSettings.customModels);
173178

0 commit comments

Comments
 (0)