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

@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Onboard/channels: recover externalized channel plugins from stale `channels.<id>` config by falling back to `ensureChannelSetupPluginInstalled` via the trusted catalog when the plugin is missing on disk, so leftover `appId`/token entries no longer dead-end onboard with "<channel> plugin not available." (#78328) Thanks @sliverp.
- Codex/app-server: forward the OpenClaw workspace bootstrap block through Codex `developerInstructions` instead of `config.instructions`, so persona/style guidance reaches the behavior-shaping app-server lane. Fixes #77363. Thanks @lonexreb.
- Dependencies: override transitive `ip-address` to `10.2.0` so the runtime lockfile no longer includes the vulnerable `10.1.0` build flagged by Dependabot alert 109. Thanks @vincentkoc.
- Feishu: hydrate missing native topic starter thread IDs before session routing so first turns and follow-ups stay in the same topic session. Fixes #78262. Thanks @joeyzenghuan.
- LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316.

View File

@@ -274,9 +274,9 @@ filenames for persona files, because Codex fallbacks only apply when
For OpenClaw workspace parity, the Codex harness resolves the other bootstrap
files (`SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`,
`BOOTSTRAP.md`, and `MEMORY.md` when present) and forwards them through Codex
config instructions on `thread/start` and `thread/resume`. This keeps
`SOUL.md` and related workspace persona/profile context visible without
duplicating `AGENTS.md`.
developer instructions on `thread/start` and `thread/resume`. This keeps
`SOUL.md` and related workspace persona/profile context visible on the native
Codex behavior-shaping lane without duplicating `AGENTS.md`.
## Add Codex alongside other models

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;