diff --git a/apps/server/src/provider/CodexDeveloperInstructions.ts b/apps/server/src/provider/CodexDeveloperInstructions.ts
index b53e1569ef..fcb2a480c0 100644
--- a/apps/server/src/provider/CodexDeveloperInstructions.ts
+++ b/apps/server/src/provider/CodexDeveloperInstructions.ts
@@ -187,3 +187,21 @@ export function buildCodexDeveloperInstructions(
deviceAvailable: tools.device === true,
})}`;
}
+
+/**
+ * Pylon's own instructions for a Codex thread, without Codex's mode template.
+ * Sent once on `thread/start` and `thread/resume`: Codex 0.153 no longer applies
+ * the developer instructions carried inside a turn's collaboration mode.
+ */
+export function buildCodexThreadInstructions(
+ toolsAvailable: boolean | T3CodeToolAvailability = true,
+): string {
+ const tools = normalizeAvailability(toolsAvailable);
+ return buildRuntimeInstructions({
+ harness: "Codex",
+ delegationAvailable: tools.delegation === true,
+ pairActive: tools.pair === true,
+ browserAvailable: tools.browser === true,
+ deviceAvailable: tools.device === true,
+ });
+}
diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
index bb5b9b5f28..5d9efaf070 100644
--- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
+++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
@@ -1035,6 +1035,64 @@ describe("openCodexThread", () => {
}),
);
+ it.effect("sends Pylon's instructions when a thread is resumed and when it is started", () =>
+ Effect.gen(function* () {
+ // Codex 0.153 ignores developer instructions carried inside a turn's
+ // collaboration mode, so without these the model never learns it runs in Pylon.
+ const payloads: Array<{ method: string; developerInstructions: unknown }> = [];
+ const record = (method: string, payload: object) =>
+ payloads.push({
+ method,
+ developerInstructions:
+ "developerInstructions" in payload ? payload.developerInstructions : undefined,
+ });
+ const client = {
+ raw: {
+ request: (
+ method: "thread/resume",
+ payload: CodexRpc.ClientRequestParamsByMethod["thread/resume"],
+ ) => {
+ record(method, payload);
+ return Effect.fail(
+ new CodexErrors.CodexAppServerRequestError({
+ code: -32603,
+ errorMessage: "thread not found",
+ }),
+ );
+ },
+ },
+ request: (
+ method: "thread/start",
+ payload: CodexRpc.ClientRequestParamsByMethod["thread/start"],
+ ) => {
+ record(method, payload);
+ return Effect.succeed(makeThreadOpenResponse("fresh-thread"));
+ },
+ };
+ const open = (developerInstructions: string | undefined) =>
+ openCodexThread({
+ client,
+ threadId: ThreadId.make("thread-1"),
+ runtimeMode: "full-access",
+ cwd: "/tmp/project",
+ requestedModel: "gpt-5.3-codex",
+ serviceTier: undefined,
+ resumeThreadId: "stale-thread",
+ developerInstructions,
+ });
+
+ yield* open("Pylon");
+ yield* open(undefined);
+
+ NodeAssert.deepStrictEqual(payloads, [
+ { method: "thread/resume", developerInstructions: "Pylon" },
+ { method: "thread/start", developerInstructions: "Pylon" },
+ { method: "thread/resume", developerInstructions: undefined },
+ { method: "thread/start", developerInstructions: undefined },
+ ]);
+ }),
+ );
+
it.effect("propagates non-recoverable resume failures", () =>
Effect.gen(function* () {
const client = {
diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts
index 31d28d8236..a82f8eab81 100644
--- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts
+++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts
@@ -51,6 +51,7 @@ import { resolveProviderHomePath } from "../../pathExpansion.ts";
import {
buildCodexDeveloperInstructions,
type T3CodeToolAvailability,
+ buildCodexThreadInstructions,
} from "../CodexDeveloperInstructions.ts";
const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);
@@ -583,10 +584,12 @@ function buildThreadStartParams(input: {
readonly runtimeMode: RuntimeMode;
readonly model: string | undefined;
readonly serviceTier: CodexServiceTier | undefined;
+ readonly developerInstructions?: string | undefined;
}): EffectCodexSchema.V2ThreadStartParams {
const config = runtimeModeToThreadConfig(input.runtimeMode);
return {
cwd: input.cwd,
+ ...(input.developerInstructions ? { developerInstructions: input.developerInstructions } : {}),
approvalPolicy: config.approvalPolicy,
sandbox: config.sandbox,
approvalsReviewer: config.approvalsReviewer,
@@ -761,6 +764,7 @@ export const openCodexThread = (input: {
readonly serviceTier: CodexServiceTier | undefined;
readonly resumeThreadId: string | undefined;
readonly strictResume?: boolean;
+ readonly developerInstructions?: string | undefined;
}): Effect.Effect => {
const resumeThreadId = input.resumeThreadId;
const startParams = buildThreadStartParams({
@@ -768,6 +772,7 @@ export const openCodexThread = (input: {
runtimeMode: input.runtimeMode,
model: input.requestedModel,
serviceTier: input.serviceTier,
+ developerInstructions: input.developerInstructions,
});
if (resumeThreadId === undefined) {
@@ -2644,6 +2649,9 @@ export const makeCodexSessionRuntime = (
serviceTier: options.serviceTier,
resumeThreadId,
...(options.strictResume ? { strictResume: true } : {}),
+ developerInstructions: buildCodexThreadInstructions(
+ configuredMcpToolAvailability(options.appServerArgs, options.mcpCapabilities),
+ ),
});
if (
options.strictResume &&
diff --git a/apps/server/src/provider/RuntimeInstructions.test.ts b/apps/server/src/provider/RuntimeInstructions.test.ts
index a21cce3793..7b81d7e681 100644
--- a/apps/server/src/provider/RuntimeInstructions.test.ts
+++ b/apps/server/src/provider/RuntimeInstructions.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vite-plus/test";
+import { buildCodexThreadInstructions } from "./CodexDeveloperInstructions.ts";
import { PAIR_LEAD_PROTOCOL, buildRuntimeInstructions } from "./RuntimeInstructions.ts";
describe("buildRuntimeInstructions", () => {
@@ -141,3 +142,21 @@ describe("buildRuntimeInstructions", () => {
expect(instructions).toContain("agent-device");
});
});
+
+describe("the instructions a Codex thread starts with", () => {
+ it("are Pylon's own, for the tools this session has, without Codex's mode template", () => {
+ expect(buildCodexThreadInstructions({ browser: true, device: false, pair: true })).toBe(
+ buildRuntimeInstructions({
+ harness: "Codex",
+ browserAvailable: true,
+ deviceAvailable: false,
+ delegationAvailable: false,
+ pairActive: true,
+ }),
+ );
+ const none = buildCodexThreadInstructions(false);
+ expect(none).toContain("running in Pylon through the Codex harness");
+ expect(none).not.toContain("");
+ expect(none).not.toContain("");
+ });
+});
diff --git a/docs/internals/delegation.md b/docs/internals/delegation.md
index 7df50929e7..226b78cdbb 100644
--- a/docs/internals/delegation.md
+++ b/docs/internals/delegation.md
@@ -113,9 +113,11 @@ Two providers cannot lead and are refused by `pair_start` and by the clients, th
executor. Antigravity offers no per-session control over its own subagents. Codex has the two flags
above, but Codex 0.153.4 keeps all six `collaboration.*` tools with both off (checked with
`codex exec -c features.multi_agent=false -c features.multi_agent_v2=false`), and in four live runs a
-paired Codex lead spawned its own subagent on its own model and never called a pair tool. It also
-reported seeing neither the pair protocol nor the `t3-code` tools, although the provider log shows
-the server ready and the protocol sent in the turn's developer instructions; why is not yet known.
+paired Codex lead spawned its own subagent on its own model and never called a pair tool. Part of
+the cause was that Pylon's instructions did not reach Codex at all (see the Codex protocol traps in
+[providers](providers.md)); its `t3-code` tools are deferred behind tool discovery, so a lead that is
+never told about them does not find them. Codex stays refused until a paired run on a fresh thread
+shows it briefing the executor with its collaboration tools still present.
A Claude lead was verified end to end: `pair_handoff`, `pair_await`, the executor's own session
writing the file, and the lead checking it.
diff --git a/docs/internals/providers.md b/docs/internals/providers.md
index e39e4f4439..c60e294a17 100644
--- a/docs/internals/providers.md
+++ b/docs/internals/providers.md
@@ -85,6 +85,16 @@ no pending RPC response to send. Blocking questions still use the request/respon
[decider](../../apps/server/src/orchestration/decider.ts) records an async answer and its user
message together.
+Codex takes Pylon's instructions twice. `thread/start` and `thread/resume` carry them as
+`developerInstructions` ([`buildCodexThreadInstructions`](../../apps/server/src/provider/CodexDeveloperInstructions.ts)),
+and each `turn/start` still carries them inside the collaboration mode's `developer_instructions`
+together with Codex's own mode template. Only the first reaches the model on Codex 0.153.4, which
+lists `collaboration_modes` as a removed feature: asked where it was running, a fresh thread answered
+"the Codex application" without the thread-level copy and "Pylon through the Codex harness" with it.
+The per-turn copy stays for older Codex versions. Whether `thread/resume` applies new instructions
+to an existing conversation is not verified. Do not test delivery by asking a model to quote its
+instructions; it answers "none" either way. Ask something only the instructions could tell it.
+
An async question can outlive the turn or a server restart. The engine reads that request's
durable activity before resolving it because the in-memory command snapshot omits old activities.
Do not infer that a request has disappeared merely because it is outside the recent window.