authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-24 21:29:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 21:29:20-07:00
log6391be2729566a1fe1045ff8617f003580b37bfd
treead41c538db15f48d02303b4d9d682915c269df74
parentab70c8943f48c08e67e3597f62941c169bc58c16
signature Signed by PGP key B5690EEEBB952194

fix(server): newer Codex models get T3 Code's instructions again (#13547)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

9 files changed, 322 insertions(+), 149 deletions(-)

apps/server/src/provider/CodexDeveloperInstructions.ts+43-33
...@@ -1,23 +1,18 @@...@@ -1,23 +1,18 @@
1import type { ProviderInteractionMode } from "@t3tools/contracts";1import type { ProviderInteractionMode } from "@t3tools/contracts";
2import type { V2TurnStartParams__AdditionalContextEntry } from "effect-codex-app-server/schema";
2import { buildRuntimeInstructions } from "./RuntimeInstructions.ts";3import { buildRuntimeInstructions } from "./RuntimeInstructions.ts";
34
4const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `5const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `## T3 Code collaborative browser
5
6## T3 Code collaborative browser
76
8You are running inside T3 Code. The \`t3-code\` MCP server is the product-native collaborative browser shared with the user. When it exposes \`preview_*\` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.7You are running inside T3 Code. The \`t3-code\` MCP server is the product-native collaborative browser shared with the user. When it exposes \`preview_*\` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.
98
10For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.9For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.
1110
12Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.11Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.`;
13`;
14
15const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = `
1612
17## T3 Code devices13const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = `## T3 Code devices
1814
19The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route.15The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route.`;
20`;
2116
22export interface T3CodeToolAvailability {17export interface T3CodeToolAvailability {
23 readonly browser: boolean;18 readonly browser: boolean;
...@@ -36,16 +31,17 @@ const normalizeAvailability = (...@@ -36,16 +31,17 @@ const normalizeAvailability = (
36 * from Playwright, agent-browser, and raw simctl/adb, so leaving them in would31 * from Playwright, agent-browser, and raw simctl/adb, so leaving them in would
37 * talk it out of the only automation it still has.32 * talk it out of the only automation it still has.
38 */33 */
39const browserToolInstructions = (availability: boolean | T3CodeToolAvailability): string => {34const toolInstructions = (availability: boolean | T3CodeToolAvailability): string => {
40 const tools = normalizeAvailability(availability);35 const tools = normalizeAvailability(availability);
41 return `${tools.browser ? T3_CODE_BROWSER_TOOL_INSTRUCTIONS : ""}${36 return [
42 tools.device ? T3_CODE_DEVICE_TOOL_INSTRUCTIONS : ""37 tools.browser ? T3_CODE_BROWSER_TOOL_INSTRUCTIONS : "",
43 }`;38 tools.device ? T3_CODE_DEVICE_TOOL_INSTRUCTIONS : "",
39 ]
40 .filter(Boolean)
41 .join("\n\n");
44};42};
4543
46const codexPlanModeDeveloperInstructions = (44const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational)
47 browserToolsAvailable: boolean | T3CodeToolAvailability,
48): string => `<collaboration_mode># Plan Mode (Conversational)
4945
50You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.46You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.
5147
...@@ -173,12 +169,9 @@ Do not ask "should I proceed?" in the final output. The user can easily switch o...@@ -173,12 +169,9 @@ Do not ask "should I proceed?" in the final output. The user can easily switch o
173Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec.169Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec.
174170
175If the user stays in Plan mode and asks for revisions after a prior \`<proposed_plan>\`, any new \`<proposed_plan>\` must be a complete replacement. If the user indicates that the prior plan is not acceptable but does not provide enough information to produce a complete replacement, address the concern and continue planning without producing a \`<proposed_plan>\` block. If the follow-up neither requires changes nor calls the plan into question (e.g. clarifying question), answer it before the block, then reproduce the prior \`<proposed_plan>\` unchanged.171If the user stays in Plan mode and asks for revisions after a prior \`<proposed_plan>\`, any new \`<proposed_plan>\` must be a complete replacement. If the user indicates that the prior plan is not acceptable but does not provide enough information to produce a complete replacement, address the concern and continue planning without producing a \`<proposed_plan>\` block. If the follow-up neither requires changes nor calls the plan into question (e.g. clarifying question), answer it before the block, then reproduce the prior \`<proposed_plan>\` unchanged.
176${browserToolInstructions(browserToolsAvailable)}
177</collaboration_mode>`;172</collaboration_mode>`;
178173
179const codexDefaultModeDeveloperInstructions = (174const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Collaboration Mode: Default
180 browserToolsAvailable: boolean | T3CodeToolAvailability,
181): string => `<collaboration_mode># Collaboration Mode: Default
182175
183You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.176You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
184177
...@@ -189,29 +182,46 @@ Your active mode changes only when new developer instructions with a different \...@@ -189,29 +182,46 @@ Your active mode changes only when new developer instructions with a different \
189Use the \`request_user_input\` tool only when it is listed in the available tools for this turn.182Use the \`request_user_input\` tool only when it is listed in the available tools for this turn.
190183
191In 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.184In 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.
192${browserToolInstructions(browserToolsAvailable)}
193</collaboration_mode>`;185</collaboration_mode>`;
194186
195export interface CodexRuntimeInfo {187export interface CodexRuntimeInfo {
196 readonly model: string;188 readonly model: string;
189 readonly modelName?: string | undefined;
197 readonly reasoningEffort: string;190 readonly reasoningEffort: string;
198}191}
199192
200export function buildCodexDeveloperInstructions(193/** Mode prompt for `turn/start.collaborationMode.settings.developer_instructions`. */
201 interactionMode: ProviderInteractionMode,194export function buildCodexDeveloperInstructions(interactionMode: ProviderInteractionMode): string {
195 return interactionMode === "plan"
196 ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
197 : CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
198}
199
200/**
201 * T3 Code context for `turn/start.additionalContext`. Codex renders each entry
202 * as a `<key>value</key>` developer message and resends it only when the value
203 * changes.
204 *
205 * This must stay out of the collaboration mode: when the model catalog ships
206 * its own text for a mode, as newer models do, Codex uses that text and drops
207 * the client's `developer_instructions` entirely.
208 */
209export function buildCodexAdditionalContext(
202 runtime: CodexRuntimeInfo,210 runtime: CodexRuntimeInfo,
203 /**211 /**
204 * Whether the `t3-code` MCP server is attached to this turn. Callers derive212 * Whether the `t3-code` MCP server is attached to this turn. Callers derive
205 * it from the session's actual MCP configuration rather than re-reading the213 * it from the session's actual MCP configuration rather than re-reading the
206 * setting, so the prompt cannot claim tools the turn doesn't have.214 * setting, so the prompt cannot claim tools the turn doesn't have.
207 */215 */
208 browserToolsAvailable: boolean | T3CodeToolAvailability = true,216 toolsAvailable: boolean | T3CodeToolAvailability = true,
209): string {217): Record<string, V2TurnStartParams__AdditionalContextEntry> {
210 const base =218 const tools = toolInstructions(toolsAvailable);
211 interactionMode === "plan"219 // Separate keys keep each value under Codex's per-entry token cap.
212 ? codexPlanModeDeveloperInstructions(browserToolsAvailable)220 return {
213 : codexDefaultModeDeveloperInstructions(browserToolsAvailable);221 t3_code_runtime: {
214 return `${base}222 kind: "application",
215223 value: buildRuntimeInstructions({ harness: "Codex", ...runtime }),
216${buildRuntimeInstructions({ harness: "Codex", ...runtime })}`;224 },
225 ...(tools ? { t3_code_tools: { kind: "application", value: tools } } : {}),
226 };
217}227}
apps/server/src/provider/Drivers/CodexDriver.ts+14-17
...@@ -172,18 +172,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -172,18 +172,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
172 ),172 ),
173 );173 );
174174
175 // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error
176 // channels at construction time — their failure modes are all on the
177 // per-operation closures they return. No `mapError` wrapper is needed
178 // here; the registry only has to worry about snapshot-build and
179 // spawner-availability failures surfaced from `checkCodexProviderStatus`
180 // below.
181 const adapter = yield* makeCodexAdapter(effectiveConfig, {
182 instanceId,
183 environment: processEnv,
184 ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
185 });
186
187 // Build a managed snapshot whose settings never change — mutations come175 // Build a managed snapshot whose settings never change — mutations come
188 // in as instance rebuilds from the registry rather than in-place176 // in as instance rebuilds from the registry rather than in-place
189 // updates. Pre-provide `ChildProcessSpawner` so the check fits177 // updates. Pre-provide `ChildProcessSpawner` so the check fits
...@@ -238,11 +226,20 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -238,11 +226,20 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
238 }),226 }),
239 ),227 ),
240 );228 );
241 const textGeneration = yield* makeCodexTextGeneration(229 const models = snapshot.getSnapshot.pipe(Effect.map((value) => value.models));
242 effectiveConfig,230 // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error
243 processEnv,231 // channels at construction time — their failure modes are all on the
244 snapshot.getSnapshot.pipe(Effect.map((value) => value.models)),232 // per-operation closures they return. No `mapError` wrapper is needed
245 );233 // here; the registry only has to worry about snapshot-build and
234 // spawner-availability failures surfaced from `checkCodexProviderStatus`
235 // above.
236 const adapter = yield* makeCodexAdapter(effectiveConfig, {
237 instanceId,
238 environment: processEnv,
239 models,
240 ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
241 });
242 const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv, models);
246 const snapshotForCwd = (cwd: string) =>243 const snapshotForCwd = (cwd: string) =>
247 !effectiveConfig.enabled244 !effectiveConfig.enabled
248 ? snapshot.getSnapshot245 ? snapshot.getSnapshot
apps/server/src/provider/Layers/CodexAdapter.ts+4
...@@ -22,6 +22,7 @@ import {...@@ -22,6 +22,7 @@ import {
22 type ToolActivityNativeAppReference,22 type ToolActivityNativeAppReference,
23 type ToolActivitySource,23 type ToolActivitySource,
24 type ProviderUserInputAnswers,24 type ProviderUserInputAnswers,
25 type ServerProviderModel,
25 RuntimeItemId,26 RuntimeItemId,
26 RuntimeRequestId,27 RuntimeRequestId,
27 RuntimeTaskId,28 RuntimeTaskId,
...@@ -89,6 +90,8 @@ const PROVIDER = ProviderDriverKind.make("codex");...@@ -89,6 +90,8 @@ const PROVIDER = ProviderDriverKind.make("codex");
89export interface CodexAdapterLiveOptions {90export interface CodexAdapterLiveOptions {
90 readonly instanceId?: ProviderInstanceId;91 readonly instanceId?: ProviderInstanceId;
91 readonly environment?: NodeJS.ProcessEnv;92 readonly environment?: NodeJS.ProcessEnv;
93 /** The provider's model list; supplies model display names for runtime info. */
94 readonly models?: Effect.Effect<ReadonlyArray<ServerProviderModel>>;
92 readonly makeRuntime?: (95 readonly makeRuntime?: (
93 options: CodexSessionRuntimeOptions,96 options: CodexSessionRuntimeOptions,
94 ) => Effect.Effect<97 ) => Effect.Effect<
...@@ -2277,6 +2280,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -2277,6 +2280,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
2277 providerInstanceId: boundInstanceId,2280 providerInstanceId: boundInstanceId,
2278 cwd: input.cwd ?? process.cwd(),2281 cwd: input.cwd ?? process.cwd(),
2279 binaryPath: codexConfig.binaryPath,2282 binaryPath: codexConfig.binaryPath,
2283 ...(options?.models ? { models: options.models } : {}),
2280 launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment),2284 launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment),
2281 ...(options?.environment ? { environment: options.environment } : {}),2285 ...(options?.environment ? { environment: options.environment } : {}),
2282 ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),2286 ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
apps/server/src/provider/Layers/CodexCollabRuntime.integration.test.ts+73
...@@ -868,3 +868,76 @@ describe("CodexSessionRuntime collab integration", () => {...@@ -868,3 +868,76 @@ describe("CodexSessionRuntime collab integration", () => {
868 );868 );
869 }869 }
870});870});
871
872describe("CodexSessionRuntime compaction", () => {
873 it.effect("restores T3 context after the root thread compacts", () =>
874 Effect.gen(function* () {
875 const compacted = (threadId: string) => ({
876 method: "item/completed",
877 params: {
878 threadId,
879 turnId: `${threadId}-turn`,
880 completedAtMs: 0,
881 item: { type: "contextCompaction", id: `compaction-${threadId}` },
882 },
883 });
884 const script = {
885 rootThreadId: ROOT,
886 recordRequests: true,
887 // A child's compaction must not inject into the root thread.
888 notifications: [compacted(CHILD_A), compacted(ROOT)],
889 };
890 // @effect-diagnostics-next-line preferSchemaOverJson:off
891 NodeFS.writeFileSync(scriptPath, JSON.stringify(script), "utf8");
892 NodeFS.rmSync(`${scriptPath}.requests`, { force: true });
893 yield* Effect.addFinalizer(() =>
894 Effect.sync(() => {
895 NodeFS.rmSync(scriptPath, { force: true });
896 NodeFS.rmSync(`${scriptPath}.requests`, { force: true });
897 }),
898 );
899
900 const runtime = yield* makeCodexSessionRuntime({
901 threadId: ThreadId.make("thread-compaction-context"),
902 binaryPath: peerPath,
903 cwd: NodeOS.tmpdir(),
904 runtimeMode: "full-access",
905 environment: { ...process.env, T3_CODEX_COLLAB_SCRIPT: scriptPath },
906 models: Effect.succeed([
907 { slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", isCustom: false, capabilities: null },
908 ]),
909 });
910 const completedFiber = yield* runtime.events.pipe(
911 Stream.filter((event) => event.method === "turn/completed"),
912 Stream.take(1),
913 Stream.runCollect,
914 Effect.forkScoped,
915 );
916
917 yield* runtime.start();
918 yield* runtime.sendTurn({ input: "keep going", interactionMode: "default" });
919 yield* Fiber.join(completedFiber);
920
921 // The restore is awaited before later notifications, so it has landed.
922 const requests = readRecordedRequests();
923 assert.lengthOf(requests, 1);
924 const [inject] = requests;
925 assert.isDefined(inject);
926 assert.equal(inject.method, "thread/inject_items");
927 assert.equal(inject.params.threadId, ROOT);
928 const texts = (
929 inject.params.items as ReadonlyArray<{ role: string; content: [{ text: string }] }>
930 ).map((item) => {
931 assert.equal(item.role, "developer");
932 return item.content[0].text;
933 });
934 assert.lengthOf(texts, 1);
935 assert.match(
936 texts[0] ?? "",
937 /^<t3_code_runtime><runtime_info>.*as GPT-5\.6 Sol \(model slug: gpt-5\.6-sol\).*<\/t3_code_runtime>$/s,
938 );
939
940 yield* runtime.close;
941 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
942 );
943});
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+90-82
...@@ -9,7 +9,10 @@ import * as CodexErrors from "effect-codex-app-server/errors";...@@ -9,7 +9,10 @@ import * as CodexErrors from "effect-codex-app-server/errors";
9import * as CodexRpc from "effect-codex-app-server/rpc";9import * as CodexRpc from "effect-codex-app-server/rpc";
10import * as EffectCodexSchema from "effect-codex-app-server/schema";10import * as EffectCodexSchema from "effect-codex-app-server/schema";
1111
12import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts";12import {
13 buildCodexAdditionalContext,
14 buildCodexDeveloperInstructions,
15} from "../CodexDeveloperInstructions.ts";
13import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";16import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";
14import {17import {
15 buildTurnStartParams,18 buildTurnStartParams,
...@@ -232,12 +235,13 @@ describe("buildTurnStartParams", () => {...@@ -232,12 +235,13 @@ describe("buildTurnStartParams", () => {
232 settings: {235 settings: {
233 model: "gpt-5.3-codex",236 model: "gpt-5.3-codex",
234 reasoning_effort: "medium",237 reasoning_effort: "medium",
235 developer_instructions: buildCodexDeveloperInstructions("plan", {238 developer_instructions: buildCodexDeveloperInstructions("plan"),
236 model: "gpt-5.3-codex",
237 reasoningEffort: "medium",
238 }),
239 },239 },
240 },240 },
241 additionalContext: buildCodexAdditionalContext({
242 model: "gpt-5.3-codex",
243 reasoningEffort: "medium",
244 }),
241 });245 });
242 });246 });
243247
...@@ -281,12 +285,13 @@ describe("buildTurnStartParams", () => {...@@ -281,12 +285,13 @@ describe("buildTurnStartParams", () => {
281 settings: {285 settings: {
282 model: "gpt-5.3-codex",286 model: "gpt-5.3-codex",
283 reasoning_effort: "medium",287 reasoning_effort: "medium",
284 developer_instructions: buildCodexDeveloperInstructions("default", {288 developer_instructions: buildCodexDeveloperInstructions("default"),
285 model: "gpt-5.3-codex",
286 reasoningEffort: "medium",
287 }),
288 },289 },
289 },290 },
291 additionalContext: buildCodexAdditionalContext({
292 model: "gpt-5.3-codex",
293 reasoningEffort: "medium",
294 }),
290 });295 });
291 });296 });
292297
...@@ -303,9 +308,29 @@ describe("buildTurnStartParams", () => {...@@ -303,9 +308,29 @@ describe("buildTurnStartParams", () => {
303 const settings = params.collaborationMode?.settings;308 const settings = params.collaborationMode?.settings;
304 NodeAssert.equal(settings?.model, DEFAULT_MODEL);309 NodeAssert.equal(settings?.model, DEFAULT_MODEL);
305 NodeAssert.equal(settings?.reasoning_effort, "medium");310 NodeAssert.equal(settings?.reasoning_effort, "medium");
306 NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));311 NodeAssert.ok(
312 params.additionalContext?.t3_code_runtime?.value.includes(`as ${DEFAULT_MODEL} with medium`),
313 );
307 });314 });
308315
316 it.effect("names the model by display name and slug in the runtime context", () =>
317 Effect.gen(function* () {
318 const params = yield* buildTurnStartParams({
319 threadId: "provider-thread-1",
320 runtimeMode: "full-access",
321 model: "gpt-5.3-codex",
322 modelName: "GPT-5.3-Codex",
323 effort: "high",
324 interactionMode: "plan",
325 });
326
327 NodeAssert.match(
328 params.additionalContext?.t3_code_runtime?.value ?? "",
329 /as GPT-5\.3-Codex \(model slug: gpt-5\.3-codex\) with high reasoning effort/,
330 );
331 }),
332 );
333
309 it.effect("routes approvals to the auto reviewer in auto mode", () =>334 it.effect("routes approvals to the auto reviewer in auto mode", () =>
310 Effect.gen(function* () {335 Effect.gen(function* () {
311 const params = yield* buildTurnStartParams({336 const params = yield* buildTurnStartParams({
...@@ -560,99 +585,82 @@ describe("Codex MCP elicitation approvals", () => {...@@ -560,99 +585,82 @@ describe("Codex MCP elicitation approvals", () => {
560});585});
561586
562describe("buildCodexDeveloperInstructions", () => {587describe("buildCodexDeveloperInstructions", () => {
563 it("appends runtime info after the mode instructions", () => {588 it("keeps T3 context out of the mode prompt, which the model catalog can replace", () => {
564 const instructions = buildCodexDeveloperInstructions("default", {
565 model: "gpt-5.3-codex",
566 reasoningEffort: "high",
567 });
568
569 NodeAssert.match(instructions, /^<collaboration_mode># Collaboration Mode: Default/);
570 NodeAssert.match(instructions, /T3 Code/);
571 NodeAssert.match(instructions, /Codex harness/);
572 NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/);
573 });
574
575 it("describes Markdown media support in the runtime context in both modes", () => {
576 for (const mode of ["default", "plan"] as const) {589 for (const mode of ["default", "plan"] as const) {
577 const instructions = buildCodexDeveloperInstructions(mode, {590 const instructions = buildCodexDeveloperInstructions(mode);
578 model: "gpt-5.3-codex",591 NodeAssert.match(instructions, /^<collaboration_mode>[\s\S]*<\/collaboration_mode>$/);
579 reasoningEffort: "high",592 NodeAssert.doesNotMatch(instructions, /runtime_info|pull_request_linking|preview_|device_/);
580 });
581 NodeAssert.match(
582 instructions,
583 /<runtime_info>.*embed images and videos.*Markdown.*<\/runtime_info>/,
584 );
585 }593 }
586 });594 });
595});
587596
588 it("includes runtime info alongside plan mode instructions", () => {597describe("buildCodexAdditionalContext", () => {
589 const instructions = buildCodexDeveloperInstructions("plan", {598 const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" };
590 model: "gpt-5.3-codex",599 const runtimeValue = (context: ReturnType<typeof buildCodexAdditionalContext>) =>
591 reasoningEffort: "medium",600 context.t3_code_runtime?.value ?? "";
592 });601
602 it("describes the harness, model, effort, and Markdown media support", () => {
603 const context = buildCodexAdditionalContext(runtime);
593604
594 NodeAssert.match(instructions, /^<collaboration_mode># Plan Mode/);605 NodeAssert.equal(context.t3_code_runtime?.kind, "application");
595 NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/);606 NodeAssert.match(
607 runtimeValue(context),
608 /<runtime_info>.*Codex harness, as gpt-5\.3-codex with high reasoning effort.*embed images and videos.*Markdown.*<\/runtime_info>/,
609 );
596 });610 });
597611
598 it("varies with the model and effort of each turn", () => {612 it("varies with the model and effort of each turn", () => {
599 const first = buildCodexDeveloperInstructions("default", {613 NodeAssert.notEqual(
600 model: "gpt-5.3-codex",614 runtimeValue(
601 reasoningEffort: "medium",615 buildCodexAdditionalContext({ model: "gpt-5.3-codex", reasoningEffort: "medium" }),
602 });616 ),
603 const second = buildCodexDeveloperInstructions("default", {617 runtimeValue(buildCodexAdditionalContext({ model: "gpt-5.4", reasoningEffort: "high" })),
604 model: "gpt-5.4",618 );
605 reasoningEffort: "high",
606 });
607
608 NodeAssert.notEqual(first, second);
609 });619 });
610620
611 it("flattens multiline metadata into single-line runtime info", () => {621 it("flattens multiline metadata into single-line runtime info", () => {
612 const instructions = buildCodexDeveloperInstructions("default", {622 const value = runtimeValue(
613 model: "gpt\n5.3\ncodex",623 buildCodexAdditionalContext({ model: "gpt\n5.3\ncodex", reasoningEffort: " high\neffort " }),
614 reasoningEffort: " high\neffort ",624 );
615 });
616625
617 NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/);626 NodeAssert.match(value, /as gpt 5\.3 codex with high effort reasoning effort/);
618 NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/);627 NodeAssert.doesNotMatch(value, /<runtime_info>[^<]*\n/);
628 });
629
630 it("keeps every entry under Codex's 1,000 token cap per entry", () => {
631 const context = buildCodexAdditionalContext(runtime, { browser: true, device: true });
632 for (const entry of Object.values(context)) {
633 // Codex estimates 4 bytes per token and truncates the middle of longer values.
634 NodeAssert.ok(Buffer.byteLength(entry.value) < 4_000);
635 }
619 });636 });
620});637});
621638
622describe("T3 browser developer instructions", () => {639describe("T3 tool instructions", () => {
623 const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" };640 const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" };
624641
625 it("prefers the product-native preview tools in both collaboration modes", () => {642 it("prefers the product-native preview tools when they are attached", () => {
626 for (const mode of ["default", "plan"] as const) {643 const tools = buildCodexAdditionalContext(runtime, true).t3_code_tools?.value ?? "";
627 const instructions = buildCodexDeveloperInstructions(mode, runtime, true);644 NodeAssert.match(tools, /t3-code/);
628 NodeAssert.match(instructions, /t3-code/);645 NodeAssert.match(tools, /preview_status/);
629 NodeAssert.match(instructions, /preview_status/);646 NodeAssert.match(tools, /preview_open/);
630 NodeAssert.match(instructions, /preview_open/);647 NodeAssert.match(tools, /Do not switch to global browser skills/);
631 NodeAssert.match(instructions, /Do not switch to global browser skills/);648 NodeAssert.doesNotMatch(tools, /device_open/);
632 }
633 });649 });
634650
635 it("omits the browser block entirely when the preview tools are not attached", () => {651 it("describes device tools only when the credential grants them", () => {
636 for (const mode of ["default", "plan"] as const) {652 const tools =
637 const instructions = buildCodexDeveloperInstructions(mode, runtime, false);653 buildCodexAdditionalContext(runtime, { browser: false, device: true }).t3_code_tools?.value ??
638 NodeAssert.doesNotMatch(instructions, /preview_status/);654 "";
639 NodeAssert.doesNotMatch(instructions, /preview_open/);655 NodeAssert.match(tools, /device_open/);
640 NodeAssert.doesNotMatch(instructions, /T3 Code collaborative browser/);656 NodeAssert.doesNotMatch(tools, /preview_open/);
641 // Steering away from other browser automation must go with the tools;
642 // keeping it would leave the model talked out of its only option.
643 NodeAssert.doesNotMatch(instructions, /Do not switch to global browser skills/);
644 // The rest of the collaboration mode is untouched.
645 NodeAssert.match(instructions, /<collaboration_mode>/);
646 NodeAssert.match(instructions, /<\/collaboration_mode>/);
647 }
648 });657 });
649658
650 it("tracks the turn's MCP configuration rather than defaulting to on", () => {659 it("omits the tool entry entirely when no tools are attached", () => {
651 NodeAssert.match(buildCodexDeveloperInstructions("default", runtime, true), /preview_open/);660 // Steering away from other browser automation must go with the tools;
652 NodeAssert.doesNotMatch(661 // keeping it would leave the model talked out of its only option.
653 buildCodexDeveloperInstructions("default", runtime, false),662 const context = buildCodexAdditionalContext(runtime, false);
654 /preview_open/,663 NodeAssert.deepStrictEqual(Object.keys(context), ["t3_code_runtime"]);
655 );
656 });664 });
657});665});
658666
apps/server/src/provider/Layers/CodexSessionRuntime.ts+72-15
...@@ -14,6 +14,7 @@ import {...@@ -14,6 +14,7 @@ import {
14 type ProviderTurnStartResult,14 type ProviderTurnStartResult,
15 type ProviderUserInputAnswers,15 type ProviderUserInputAnswers,
16 RuntimeMode,16 RuntimeMode,
17 type ServerProviderModel,
17 ThreadId,18 ThreadId,
18 TurnId,19 TurnId,
19} from "@t3tools/contracts";20} from "@t3tools/contracts";
...@@ -40,6 +41,7 @@ import { buildCodexInitializeParams } from "./CodexProvider.ts";...@@ -40,6 +41,7 @@ import { buildCodexInitializeParams } from "./CodexProvider.ts";
40import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";41import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";
41import { expandHomePath } from "../../pathExpansion.ts";42import { expandHomePath } from "../../pathExpansion.ts";
42import {43import {
44 buildCodexAdditionalContext,
43 buildCodexDeveloperInstructions,45 buildCodexDeveloperInstructions,
44 type T3CodeToolAvailability,46 type T3CodeToolAvailability,
45} from "../CodexDeveloperInstructions.ts";47} from "../CodexDeveloperInstructions.ts";
...@@ -141,10 +143,13 @@ const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata);...@@ -141,10 +143,13 @@ const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata);
141const isMcpElicitationForm = Schema.is(McpElicitationForm);143const isMcpElicitationForm = Schema.is(McpElicitationForm);
142144
143// TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated145// TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated
144// `V2TurnStartParams` schema includes `collaborationMode` directly.146// `V2TurnStartParams` schema includes its experimental fields directly.
145const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe(147const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe(
146 Schema.fieldsAssign({148 Schema.fieldsAssign({
147 collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode),149 collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode),
150 additionalContext: Schema.optionalKey(
151 Schema.Record(Schema.String, EffectCodexSchema.V2TurnStartParams__AdditionalContextEntry),
152 ),
148 }),153 }),
149);154);
150const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(155const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(
...@@ -178,6 +183,8 @@ export interface CodexSessionRuntimeOptions {...@@ -178,6 +183,8 @@ export interface CodexSessionRuntimeOptions {
178 readonly serviceTier?: CodexServiceTier | undefined;183 readonly serviceTier?: CodexServiceTier | undefined;
179 readonly resumeCursor?: CodexResumeCursor;184 readonly resumeCursor?: CodexResumeCursor;
180 readonly appServerArgs?: ReadonlyArray<string>;185 readonly appServerArgs?: ReadonlyArray<string>;
186 /** The provider's model list; supplies the display name for runtime info. */
187 readonly models?: Effect.Effect<ReadonlyArray<ServerProviderModel>>;
181 /** Capabilities the session's `t3-code` MCP credential grants; drives the prompt blocks. */188 /** Capabilities the session's `t3-code` MCP credential grants; drives the prompt blocks. */
182 readonly mcpCapabilities?: ReadonlySet<string>;189 readonly mcpCapabilities?: ReadonlySet<string>;
183}190}
...@@ -579,28 +586,31 @@ function runtimeModeToTurnSandboxPolicy(...@@ -579,28 +586,31 @@ function runtimeModeToTurnSandboxPolicy(
579 }586 }
580}587}
581588
582function buildCodexCollaborationMode(input: {589function buildCodexTurnInstructions(input: {
583 readonly interactionMode?: ProviderInteractionMode;590 readonly interactionMode?: ProviderInteractionMode;
584 readonly model?: string;591 readonly model?: string;
592 readonly modelName?: string;
585 readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;593 readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
586 readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;594 readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;
587}): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined {595}): Pick<CodexTurnStartParamsWithCollaborationMode, "collaborationMode" | "additionalContext"> {
588 if (input.interactionMode === undefined) {596 if (input.interactionMode === undefined) {
589 return undefined;597 return {};
590 }598 }
591 const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL;599 const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL;
592 const reasoningEffort = input.effort ?? "medium";600 const reasoningEffort = input.effort ?? "medium";
593 return {601 return {
594 mode: input.interactionMode,602 collaborationMode: {
595 settings: {603 mode: input.interactionMode,
596 model,604 settings: {
597 reasoning_effort: reasoningEffort,605 model,
598 developer_instructions: buildCodexDeveloperInstructions(606 reasoning_effort: reasoningEffort,
599 input.interactionMode,607 developer_instructions: buildCodexDeveloperInstructions(input.interactionMode),
600 { model, reasoningEffort },608 },
601 input.browserToolsAvailable ?? true,
602 ),
603 },609 },
610 additionalContext: buildCodexAdditionalContext(
611 { model, modelName: input.modelName, reasoningEffort },
612 input.browserToolsAvailable ?? true,
613 ),
604 };614 };
605}615}
606616
...@@ -617,6 +627,8 @@ export function buildTurnStartParams(input: {...@@ -617,6 +627,8 @@ export function buildTurnStartParams(input: {
617 readonly path: string;627 readonly path: string;
618 }>;628 }>;
619 readonly model?: string;629 readonly model?: string;
630 /** Display name of `model`, for runtime info. */
631 readonly modelName?: string;
620 readonly serviceTier?: CodexServiceTier;632 readonly serviceTier?: CodexServiceTier;
621 readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;633 readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
622 readonly interactionMode?: ProviderInteractionMode;634 readonly interactionMode?: ProviderInteractionMode;
...@@ -638,9 +650,10 @@ export function buildTurnStartParams(input: {...@@ -638,9 +650,10 @@ export function buildTurnStartParams(input: {
638 }650 }
639651
640 const config = runtimeModeToThreadConfig(input.runtimeMode);652 const config = runtimeModeToThreadConfig(input.runtimeMode);
641 const collaborationMode = buildCodexCollaborationMode({653 const turnInstructions = buildCodexTurnInstructions({
642 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),654 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
643 ...(input.model ? { model: input.model } : {}),655 ...(input.model ? { model: input.model } : {}),
656 ...(input.modelName ? { modelName: input.modelName } : {}),
644 ...(input.effort ? { effort: input.effort } : {}),657 ...(input.effort ? { effort: input.effort } : {}),
645 browserToolsAvailable: input.browserToolsAvailable ?? true,658 browserToolsAvailable: input.browserToolsAvailable ?? true,
646 });659 });
...@@ -654,7 +667,7 @@ export function buildTurnStartParams(input: {...@@ -654,7 +667,7 @@ export function buildTurnStartParams(input: {
654 ...(input.model ? { model: input.model } : {}),667 ...(input.model ? { model: input.model } : {}),
655 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),668 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
656 ...(input.effort ? { effort: input.effort } : {}),669 ...(input.effort ? { effort: input.effort } : {}),
657 ...(collaborationMode ? { collaborationMode } : {}),670 ...turnInstructions,
658 }).pipe(671 }).pipe(
659 Effect.mapError((cause) =>672 Effect.mapError((cause) =>
660 CodexErrors.CodexAppServerProtocolParseError.fromSchemaError(673 CodexErrors.CodexAppServerProtocolParseError.fromSchemaError(
...@@ -1305,6 +1318,9 @@ export const makeCodexSessionRuntime = (...@@ -1305,6 +1318,9 @@ export const makeCodexSessionRuntime = (
1305 const collabChildLiveTurnsRef = yield* Ref.make(new Map<string, string>());1318 const collabChildLiveTurnsRef = yield* Ref.make(new Map<string, string>());
1306 const suppressMemoryConsolidationNotification = makeMemoryConsolidationNotificationFilter();1319 const suppressMemoryConsolidationNotification = makeMemoryConsolidationNotificationFilter();
1307 const closedRef = yield* Ref.make(false);1320 const closedRef = yield* Ref.make(false);
1321 /** The `additionalContext` of the latest `turn/start`, restored after compaction. */
1322 const lastAdditionalContextRef =
1323 yield* Ref.make<CodexTurnStartParamsWithCollaborationMode["additionalContext"]>(undefined);
13081324
1309 // `~` is not shell-expanded when env vars are set via1325 // `~` is not shell-expanded when env vars are set via
1310 // `child_process.spawn`; `expandHomePath` lets a configured1326 // `child_process.spawn`; `expandHomePath` lets a configured
...@@ -1856,6 +1872,35 @@ export const makeCodexSessionRuntime = (...@@ -1856,6 +1872,35 @@ export const makeCodexSessionRuntime = (
1856 }1872 }
1857 });1873 });
18581874
1875 /**
1876 * Compaction rebuilds history from user messages and Codex's own context,
1877 * which drops our `additionalContext` messages. Codex only resends an
1878 * entry when its value changes, so without this the T3 context would stay
1879 * lost until the model or effort changed. Awaited so the context is back
1880 * before later notifications from the same turn are handled. Drop this if
1881 * Codex enables its `retain_client_developer_messages` feature by default.
1882 */
1883 const restoreAdditionalContext = (threadId: string) =>
1884 Effect.gen(function* () {
1885 const context = yield* Ref.get(lastAdditionalContextRef);
1886 if (!context) return;
1887 yield* client.request("thread/inject_items", {
1888 threadId,
1889 items: Object.entries(context).map(([key, entry]) => ({
1890 type: "message",
1891 role: "developer",
1892 content: [{ type: "input_text", text: `<${key}>${entry.value}</${key}>` }],
1893 })),
1894 });
1895 }).pipe(
1896 Effect.timeout("10 seconds"),
1897 Effect.catch((cause) =>
1898 Effect.logWarning("Failed to restore Codex additional context after compaction.", {
1899 cause,
1900 }),
1901 ),
1902 );
1903
1859 const handleRawNotification = (notification: CodexServerNotification) =>1904 const handleRawNotification = (notification: CodexServerNotification) =>
1860 Effect.gen(function* () {1905 Effect.gen(function* () {
1861 const isMemoryConsolidationNotification =1906 const isMemoryConsolidationNotification =
...@@ -1942,6 +1987,14 @@ export const makeCodexSessionRuntime = (...@@ -1942,6 +1987,14 @@ export const makeCodexSessionRuntime = (
1942 return;1987 return;
1943 }1988 }
19441989
1990 if (
1991 notification.method === "item/completed" &&
1992 notification.params.item.type === "contextCompaction" &&
1993 notification.params.threadId === suppressRootId
1994 ) {
1995 yield* restoreAdditionalContext(notification.params.threadId);
1996 }
1997
1945 let requestId: ApprovalRequestId | undefined;1998 let requestId: ApprovalRequestId | undefined;
1946 let requestKind: ProviderRequestKind | undefined;1999 let requestKind: ProviderRequestKind | undefined;
1947 let turnId = childParentTurnId ?? route.turnId;2000 let turnId = childParentTurnId ?? route.turnId;
...@@ -2509,12 +2562,15 @@ export const makeCodexSessionRuntime = (...@@ -2509,12 +2562,15 @@ export const makeCodexSessionRuntime = (
2509 const normalizedModel = normalizeCodexModelSlug(2562 const normalizedModel = normalizeCodexModelSlug(
2510 input.model ?? (yield* Ref.get(sessionRef)).model,2563 input.model ?? (yield* Ref.get(sessionRef)).model,
2511 );2564 );
2565 const models = options.models ? yield* options.models : [];
2566 const modelName = models.find((model) => model.slug === normalizedModel)?.name;
2512 const params = yield* buildTurnStartParams({2567 const params = yield* buildTurnStartParams({
2513 threadId: providerThreadId,2568 threadId: providerThreadId,
2514 runtimeMode: options.runtimeMode,2569 runtimeMode: options.runtimeMode,
2515 ...(input.input ? { prompt: input.input } : {}),2570 ...(input.input ? { prompt: input.input } : {}),
2516 ...(input.attachments ? { attachments: input.attachments } : {}),2571 ...(input.attachments ? { attachments: input.attachments } : {}),
2517 ...(normalizedModel ? { model: normalizedModel } : {}),2572 ...(normalizedModel ? { model: normalizedModel } : {}),
2573 ...(modelName ? { modelName } : {}),
2518 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),2574 ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
2519 ...(input.effort ? { effort: input.effort } : {}),2575 ...(input.effort ? { effort: input.effort } : {}),
2520 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),2576 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
...@@ -2526,6 +2582,7 @@ export const makeCodexSessionRuntime = (...@@ -2526,6 +2582,7 @@ export const makeCodexSessionRuntime = (
2526 options.mcpCapabilities,2582 options.mcpCapabilities,
2527 ),2583 ),
2528 });2584 });
2585 yield* Ref.set(lastAdditionalContextRef, params.additionalContext);
2529 const rawResponse = yield* client.raw.request("turn/start", params);2586 const rawResponse = yield* client.raw.request("turn/start", params);
2530 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(2587 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
2531 Effect.mapError((error) =>2588 Effect.mapError((error) =>
apps/server/src/provider/RuntimeInstructions.test.ts+9
...@@ -20,6 +20,15 @@ describe("buildRuntimeInstructions", () => {...@@ -20,6 +20,15 @@ describe("buildRuntimeInstructions", () => {
20 ).toContain("through the Codex harness, as custom model with high reasoning effort.");20 ).toContain("through the Codex harness, as custom model with high reasoning effort.");
21 });21 });
2222
23 it("names the model by display name and slug when they differ", () => {
24 expect(
25 buildRuntimeInstructions({ harness: "Codex", model: "gpt-5.4", modelName: "GPT-5.4" }),
26 ).toContain("through the Codex harness, as GPT-5.4 (model slug: gpt-5.4).");
27 expect(
28 buildRuntimeInstructions({ harness: "Codex", model: "my-model", modelName: "my-model" }),
29 ).toContain("through the Codex harness, as my-model.");
30 });
31
23 it.each([undefined, "", "auto", "default"])("omits unresolved model %s", (model) => {32 it.each([undefined, "", "auto", "default"])("omits unresolved model %s", (model) => {
24 const instructions = buildRuntimeInstructions({ harness: "Cursor", model });33 const instructions = buildRuntimeInstructions({ harness: "Cursor", model });
25 expect(instructions).toContain("through the Cursor harness.");34 expect(instructions).toContain("through the Cursor harness.");
apps/server/src/provider/RuntimeInstructions.ts+9-2
...@@ -2,16 +2,23 @@ const PULL_REQUEST_LINKING_INSTRUCTIONS = `<pull_request_linking>...@@ -2,16 +2,23 @@ const PULL_REQUEST_LINKING_INSTRUCTIONS = `<pull_request_linking>
2When the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked.2When the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked.
3</pull_request_linking>`;3</pull_request_linking>`;
44
5/** Shared runtime context; omit model and effort when the harness manages them dynamically. */5/**
6 * Shared runtime context; omit model and effort when the harness manages them dynamically.
7 * `modelName` is the display name users see in the model picker; `model` is the slug.
8 */
6export function buildRuntimeInstructions(runtime: {9export function buildRuntimeInstructions(runtime: {
7 readonly harness: string;10 readonly harness: string;
8 readonly model?: string | undefined;11 readonly model?: string | undefined;
12 readonly modelName?: string | undefined;
9 readonly reasoningEffort?: string | undefined;13 readonly reasoningEffort?: string | undefined;
10}): string {14}): string {
11 const harness = toSingleLine(runtime.harness);15 const harness = toSingleLine(runtime.harness);
12 const model = toSingleLine(runtime.model ?? "");16 const model = toSingleLine(runtime.model ?? "");
17 const modelName = toSingleLine(runtime.modelName ?? "");
13 const effort = toSingleLine(runtime.reasoningEffort ?? "");18 const effort = toSingleLine(runtime.reasoningEffort ?? "");
14 const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${model}` : "";19 const modelLabel =
20 modelName && modelName !== model ? `${modelName} (model slug: ${model})` : model;
21 const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${modelLabel}` : "";
15 const effortInfo = effort ? ` with ${effort} reasoning effort` : "";22 const effortInfo = effort ? ` with ${effort} reasoning effort` : "";
16 return `<runtime_info>In case you're asked: you are running in T3 Code through the ${harness} harness${modelInfo}${effortInfo}. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>\n\n${PULL_REQUEST_LINKING_INSTRUCTIONS}`;23 return `<runtime_info>In case you're asked: you are running in T3 Code through the ${harness} harness${modelInfo}${effortInfo}. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>\n\n${PULL_REQUEST_LINKING_INSTRUCTIONS}`;
17}24}
apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs+8
...@@ -106,6 +106,14 @@ rl.on("line", (line) => {...@@ -106,6 +106,14 @@ rl.on("line", (line) => {
106 write({ id, result: fixture.responses.threadStart });106 write({ id, result: fixture.responses.threadStart });
107 return;107 return;
108 }108 }
109 if (method === "thread/inject_items" && script.recordRequests) {
110 NodeFS.appendFileSync(
111 `${process.env.T3_CODEX_COLLAB_SCRIPT}.requests`,
112 `${JSON.stringify({ method, params: message.params })}\n`,
113 );
114 write({ id, result: {} });
115 return;
116 }
109 if (method === "thread/resume") {117 if (method === "thread/resume") {
110 if (script.recordRequests) {118 if (script.recordRequests) {
111 NodeFS.appendFileSync(119 NodeFS.appendFileSync(