fix(codex/app-server): forward bootstrap into developerInstructions (#77372)

The OpenClaw workspace bootstrap block (SOUL.md, IDENTITY.md, USER.md,
TOOLS.md, BOOTSTRAP.md, MEMORY.md, HEARTBEAT.md) was only being merged into
Codex's config.instructions. The Codex app-server runtime overlay
consistently applies the explicit developerInstructions field, so persona
and style guidance present in the workspace was failing to shape Codex
behavior on session resume.

Build the workspace bootstrap block before finalizing developerInstructions
and join it into both:

- the baseline developerInstructions (initial assignment), and
- the context-engine developerInstructions (when context engine is active),
  preserving the existing config-engine projection addition.

The existing config.instructions merge stays intact, so the bootstrap now
reaches Codex through both paths and downstream hooks
(resolveAgentHarnessBeforePromptBuildResult) see what Codex will actually
receive. AGENTS.md remains excluded because Codex loads it natively.

Update the existing 'passes OpenClaw bootstrap files through ...' test to
also assert the developerInstructions field carries SOUL.md and the Codex
AGENTS.md substitution note while still excluding the native AGENTS.md
content.

Fixes #77363.
This commit is contained in:
Shubhankar Tripathy
2026-05-06 03:09:59 -05:00
committed by GitHub
parent af2719a7b9
commit 9edeffc751
4 changed files with 33 additions and 38 deletions

View File

@@ -952,7 +952,7 @@ describe("runCodexAppServerAttempt", () => {
expect(inputText).toContain("make the default webpage openclaw");
});
it("passes OpenClaw bootstrap files through Codex config instructions", async () => {
it("passes OpenClaw bootstrap files through Codex developer instructions", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
@@ -967,14 +967,18 @@ describe("runCodexAppServerAttempt", () => {
await run;
const threadStart = harness.requests.find((request) => request.method === "thread/start");
const config = (threadStart?.params as { config?: { instructions?: string } }).config;
expect(config).toEqual(
expect.objectContaining({
instructions: expect.stringContaining("Soul voice goes here."),
}),
);
expect(config?.instructions).toContain("Codex loads AGENTS.md natively");
expect(config?.instructions).not.toContain("Follow AGENTS guidance.");
const params = threadStart?.params as {
config?: { instructions?: string };
developerInstructions?: string;
};
const config = params.config;
// Regression for #77363: persona/style bootstrap (SOUL.md) must reach the
// explicit developerInstructions field, not config.instructions.
expect(params.developerInstructions).toContain("Soul voice goes here.");
expect(params.developerInstructions).toContain("Codex loads AGENTS.md natively");
expect(params.developerInstructions).not.toContain("Follow AGENTS guidance.");
expect(config?.instructions).toBeUndefined();
});
it("fires llm_input, llm_output, and agent_end hooks for codex turns", async () => {

View File

@@ -484,8 +484,21 @@ export async function runCodexAppServerAttempt(
(await readMirroredSessionHistoryMessages(params.sessionFile)) ?? historyMessages;
}
const baseDeveloperInstructions = buildDeveloperInstructions(params);
// Build the workspace bootstrap block before finalizing developer
// instructions so persona files (SOUL.md, IDENTITY.md, ...) reach Codex
// through the explicit `developerInstructions` field.
const workspaceBootstrapInstructions = await buildCodexWorkspaceBootstrapInstructions({
params,
resolvedWorkspace,
effectiveWorkspace,
sessionKey: sandboxSessionKey,
sessionAgentId,
});
let promptText = params.prompt;
let developerInstructions = baseDeveloperInstructions;
let developerInstructions = joinPresentSections(
baseDeveloperInstructions,
workspaceBootstrapInstructions,
);
let prePromptMessageCount = historyMessages.length;
if (activeContextEngine) {
try {
@@ -512,6 +525,7 @@ export async function runCodexAppServerAttempt(
promptText = projection.promptText;
developerInstructions = joinPresentSections(
baseDeveloperInstructions,
workspaceBootstrapInstructions,
projection.developerInstructionAddition,
);
prePromptMessageCount = projection.prePromptMessageCount;
@@ -541,13 +555,6 @@ export async function runCodexAppServerAttempt(
messages: historyMessages,
ctx: hookContext,
});
const workspaceBootstrapInstructions = await buildCodexWorkspaceBootstrapInstructions({
params,
resolvedWorkspace,
effectiveWorkspace,
sessionKey: sandboxSessionKey,
sessionAgentId,
});
const trajectoryRecorder = createCodexTrajectoryRecorder({
attempt: params,
cwd: effectiveWorkspace,
@@ -583,10 +590,7 @@ export async function runCodexAppServerAttempt(
: options.nativeHookRelay?.enabled === false
? buildCodexNativeHookRelayDisabledConfig()
: undefined;
const threadConfig = mergeCodexConfigInstructions(
nativeHookRelayConfig,
workspaceBootstrapInstructions,
);
const threadConfig = nativeHookRelayConfig;
({ client, thread } = await withCodexStartupTimeout({
timeoutMs: params.timeoutMs,
timeoutFloorMs: options.startupTimeoutFloorMs,
@@ -1871,20 +1875,6 @@ function renderCodexWorkspaceBootstrapInstructions(
return lines.join("\n").trim();
}
function mergeCodexConfigInstructions(
config: JsonObject | undefined,
instructions: string | undefined,
): JsonObject | undefined {
if (!instructions?.trim()) {
return config;
}
const merged: JsonObject = { ...config };
const existingInstructions =
typeof merged.instructions === "string" ? merged.instructions.trim() : undefined;
merged.instructions = joinPresentSections(existingInstructions, instructions);
return merged;
}
function remapCodexContextFilePath(params: {
file: EmbeddedContextFile;
sourceWorkspaceDir: string;