fix: align Codex cron bootstrap context (#81822)

* fix: align Codex cron bootstrap context

* fix: address Codex cron review comments

* fix: suppress Codex project docs for lightweight context

* fix: note Codex cron lightweight context
This commit is contained in:
Josh Lehman
2026-05-14 15:10:42 -07:00
committed by GitHub
parent bcbf4fc35f
commit 3f80f889fa
6 changed files with 109 additions and 16 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- ACP/Codex: surface redacted Codex wrapper stderr for generic ACP internal failures and preserve safe Codex model/provider routing in isolated `CODEX_HOME`, making `sessions_spawn(runtime="acp", agentId="codex")` failures actionable. Fixes #80079. (#80718) Thanks @leoge007.
- ACP: treat rejected timeout config options as best-effort hints so ACP turns continue with adapters that do not support `session/set_config_option` timeout keys. Fixes #81250. (#81603) Thanks @qkal.
- Cron/Codex: default exact-command scheduled agent turns to lightweight bootstrap context so automation runs the command before loading workspace identity or memory context.
- Codex cron: disable native Codex project-doc loading for lightweight app-server cron turns so scheduled jobs avoid project-doc injection after OpenClaw suppresses bootstrap context. (#81822) Thanks @jalehman.
- Codex plugin/Gateway: strip unpaired UTF-16 surrogates from Codex app-server JSON-RPC payloads and let stale reply-work recovery abort stalled reply runs, preventing malformed media turns from wedging gateway lanes.
- Codex app server: force OAuth refresh requests to perform a real token refresh instead of reusing unchanged inherited auth-profile tokens after refresh failures. (#80738) Thanks @simplyclever914.
- Control UI/WebChat: render `/tts audio` replies as playable audio attachments through the assistant-media ticket path, with structured-audio compatibility for older live payloads. (#81722) Thanks @Conan-Scott.

View File

@@ -2516,24 +2516,17 @@ describe("runCodexAppServerAttempt", () => {
const threadStart = harness.requests.find((request) => request.method === "thread/start");
const threadStartParams = threadStart?.params as {
developerInstructions?: string;
config?: Record<string, unknown>;
};
expect(threadStartParams.config?.project_doc_max_bytes).toBe(0);
expect(threadStartParams.developerInstructions).not.toContain("Soul voice goes here.");
expect(threadStartParams.developerInstructions).not.toContain("Follow AGENTS guidance.");
const turnStart = harness.requests.find((request) => request.method === "turn/start");
const turnStartParams = turnStart?.params as {
collaborationMode?: {
settings?: { developer_instructions?: string | null };
};
input?: Array<{ text?: string }>;
};
expect(turnStartParams.input?.[0]?.text).toBe(exactCommand);
expect(turnStartParams.collaborationMode?.settings?.developer_instructions).toContain(
"This is an OpenClaw cron automation turn",
);
expect(turnStartParams.collaborationMode?.settings?.developer_instructions).toContain(
"run that command before doing any investigation",
);
});
it("fires llm_input, llm_output, and agent_end hooks for codex turns", async () => {

View File

@@ -11,6 +11,8 @@ function createAttemptParams(params: {
authProfileId?: string;
authProfileProvider?: string;
authProfileProviders?: Record<string, string>;
bootstrapContextMode?: "full" | "lightweight";
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
}): EmbeddedRunAttemptParams {
const authProfileProviders =
params.authProfileProviders ??
@@ -21,6 +23,10 @@ function createAttemptParams(params: {
provider: params.provider,
modelId: "gpt-5.4",
authProfileId: params.authProfileId,
...(params.bootstrapContextMode ? { bootstrapContextMode: params.bootstrapContextMode } : {}),
...(params.bootstrapContextRunKind
? { bootstrapContextRunKind: params.bootstrapContextRunKind }
: {}),
authProfileStore: {
version: 1,
profiles: Object.fromEntries(
@@ -80,6 +86,53 @@ describe("Codex app-server native code mode config", () => {
"features.code_mode_only": true,
});
});
it("disables native Codex project docs for lightweight context threads", () => {
const request = buildThreadStartParams(
createAttemptParams({
provider: "openai",
bootstrapContextMode: "lightweight",
bootstrapContextRunKind: "cron",
}),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
project_doc_max_bytes: 64_000,
"features.codex_hooks": true,
},
},
);
expect(request.config).toEqual({
project_doc_max_bytes: 0,
"features.codex_hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("keeps native Codex project docs enabled when context is not lightweight", () => {
const request = buildThreadResumeParams(
createAttemptParams({ provider: "openai", bootstrapContextRunKind: "cron" }),
{
threadId: "thread-1",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
project_doc_max_bytes: 64_000,
},
},
);
expect(request.config).toEqual({
project_doc_max_bytes: 64_000,
"features.code_mode": true,
"features.code_mode_only": true,
});
});
});
describe("Codex app-server model provider selection", () => {

View File

@@ -65,6 +65,10 @@ export const CODEX_CODE_MODE_THREAD_CONFIG: JsonObject = {
"features.code_mode_only": true,
};
const CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG: JsonObject = {
project_doc_max_bytes: 0,
};
export async function startOrResumeThread(params: {
client: CodexAppServerClient;
params: EmbeddedRunAttemptParams;
@@ -472,7 +476,7 @@ export function buildThreadStartParams(
sandbox: options.appServer.sandbox,
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
serviceName: "OpenClaw",
config: buildCodexRuntimeThreadConfig(options.config),
config: buildCodexRuntimeThreadConfigForRun(params, options.config),
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
dynamicTools: options.dynamicTools,
experimentalRawEvents: true,
@@ -505,16 +509,31 @@ export function buildThreadResumeParams(
approvalsReviewer: options.appServer.approvalsReviewer,
sandbox: options.appServer.sandbox,
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
config: buildCodexRuntimeThreadConfig(options.config),
config: buildCodexRuntimeThreadConfigForRun(params, options.config),
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
persistExtendedHistory: true,
};
}
export function buildCodexRuntimeThreadConfig(config: JsonObject | undefined): JsonObject {
const runtimeConfig = mergeCodexThreadConfigs(config, CODEX_CODE_MODE_THREAD_CONFIG) ?? {
...CODEX_CODE_MODE_THREAD_CONFIG,
};
return runtimeConfig;
}
function buildCodexRuntimeThreadConfigForRun(
params: EmbeddedRunAttemptParams,
config: JsonObject | undefined,
): JsonObject {
const runtimeConfig = buildCodexRuntimeThreadConfig(config);
if (params.bootstrapContextMode !== "lightweight") {
return runtimeConfig;
}
return (
mergeCodexThreadConfigs(config, CODEX_CODE_MODE_THREAD_CONFIG) ?? {
...CODEX_CODE_MODE_THREAD_CONFIG,
mergeCodexThreadConfigs(runtimeConfig, CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG) ?? {
...runtimeConfig,
...CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG,
}
);
}

View File

@@ -152,6 +152,7 @@ export function createCronPromptExecutor(params: {
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
params.cronSession.sessionEntry.systemPromptReport,
);
const bootstrapContextMode = resolveCronBootstrapContextMode(params.agentPayload);
const runPrompt = async (promptText: string) => {
const fallbackResult = await runWithModelFallback({
@@ -202,10 +203,10 @@ export function createCronPromptExecutor(params: {
abortSignal: params.abortSignal,
onExecutionStarted: params.onExecutionStarted,
onExecutionPhase: params.onExecutionPhase,
bootstrapContextMode,
bootstrapContextRunKind: "cron",
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
bootstrapContextMode: resolveCronBootstrapContextMode(params.agentPayload),
bootstrapContextRunKind: "cron",
senderIsOwner: params.senderIsOwner,
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
@@ -260,7 +261,7 @@ export function createCronPromptExecutor(params: {
verboseLevel: params.resolvedVerboseLevel,
timeoutMs: params.timeoutMs,
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
bootstrapContextMode: resolveCronBootstrapContextMode(params.agentPayload),
bootstrapContextMode,
bootstrapContextRunKind: "cron",
toolsAllow: params.agentPayload?.toolsAllow,
execOverrides: params.suppressExecNotifyOnExit

View File

@@ -41,6 +41,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
const result = await runCronIsolatedAgentTurn(
makeIsolatedAgentTurnParams({
sessionKey: "cron:daily-monitor",
job: makeIsolatedAgentTurnJob({
payload: {
kind: "agentTurn",
message: "test",
lightContext: true,
},
}),
}),
);
@@ -56,10 +63,14 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as {
sessionId?: string;
sessionKey?: string;
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
};
expect(runRequest.sessionId).toBe("isolated-run-1");
expect(runRequest.sessionKey).toBe("agent:default:cron:daily-monitor:run:isolated-run-1");
expect(runRequest.sessionKey).not.toBe("agent:default:cron:daily-monitor");
expect(runRequest.bootstrapContextMode).toBe("lightweight");
expect(runRequest.bootstrapContextRunKind).toBe("cron");
});
it("keeps explicit session-bound cron execution on the requested session key", async () => {
@@ -88,9 +99,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as {
sessionId?: string;
sessionKey?: string;
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
};
expect(runRequest.sessionId).toBe("bound-run-1");
expect(runRequest.sessionKey).toBe("agent:default:project-alpha-monitor");
expect(runRequest.bootstrapContextMode).toBeUndefined();
expect(runRequest.bootstrapContextRunKind).toBe("cron");
});
it("uses a run-scoped key for CLI isolated cron execution", async () => {
@@ -112,6 +127,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
const result = await runCronIsolatedAgentTurn(
makeIsolatedAgentTurnParams({
sessionKey: "cron:cli-monitor",
job: makeIsolatedAgentTurnJob({
payload: {
kind: "agentTurn",
message: "test",
lightContext: true,
},
}),
}),
);
@@ -122,11 +144,15 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
sessionId?: string;
sessionKey?: string;
senderIsOwner?: boolean;
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
};
expect(runRequest.sessionId).toBe("isolated-cli-run-1");
expect(runRequest.sessionKey).toBe("agent:default:cron:cli-monitor:run:isolated-cli-run-1");
expect(runRequest.sessionKey).not.toBe("agent:default:cron:cli-monitor");
expect(runRequest.senderIsOwner).toBe(true);
expect(runRequest.bootstrapContextMode).toBe("lightweight");
expect(runRequest.bootstrapContextRunKind).toBe("cron");
});
it("runs externally sourced CLI hook turns without owner tool authority", async () => {