Skip to content

Commit 3f80f88

Browse files
authored
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
1 parent bcbf4fc commit 3f80f88

6 files changed

Lines changed: 109 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
- 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.
2525
- 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.
2626
- Cron/Codex: default exact-command scheduled agent turns to lightweight bootstrap context so automation runs the command before loading workspace identity or memory context.
27+
- 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.
2728
- 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.
2829
- 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.
2930
- 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.

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2516,24 +2516,17 @@ describe("runCodexAppServerAttempt", () => {
25162516
const threadStart = harness.requests.find((request) => request.method === "thread/start");
25172517
const threadStartParams = threadStart?.params as {
25182518
developerInstructions?: string;
2519+
config?: Record<string, unknown>;
25192520
};
2521+
expect(threadStartParams.config?.project_doc_max_bytes).toBe(0);
25202522
expect(threadStartParams.developerInstructions).not.toContain("Soul voice goes here.");
25212523
expect(threadStartParams.developerInstructions).not.toContain("Follow AGENTS guidance.");
25222524

25232525
const turnStart = harness.requests.find((request) => request.method === "turn/start");
25242526
const turnStartParams = turnStart?.params as {
2525-
collaborationMode?: {
2526-
settings?: { developer_instructions?: string | null };
2527-
};
25282527
input?: Array<{ text?: string }>;
25292528
};
25302529
expect(turnStartParams.input?.[0]?.text).toBe(exactCommand);
2531-
expect(turnStartParams.collaborationMode?.settings?.developer_instructions).toContain(
2532-
"This is an OpenClaw cron automation turn",
2533-
);
2534-
expect(turnStartParams.collaborationMode?.settings?.developer_instructions).toContain(
2535-
"run that command before doing any investigation",
2536-
);
25372530
});
25382531

25392532
it("fires llm_input, llm_output, and agent_end hooks for codex turns", async () => {

extensions/codex/src/app-server/thread-lifecycle.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ function createAttemptParams(params: {
1111
authProfileId?: string;
1212
authProfileProvider?: string;
1313
authProfileProviders?: Record<string, string>;
14+
bootstrapContextMode?: "full" | "lightweight";
15+
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
1416
}): EmbeddedRunAttemptParams {
1517
const authProfileProviders =
1618
params.authProfileProviders ??
@@ -21,6 +23,10 @@ function createAttemptParams(params: {
2123
provider: params.provider,
2224
modelId: "gpt-5.4",
2325
authProfileId: params.authProfileId,
26+
...(params.bootstrapContextMode ? { bootstrapContextMode: params.bootstrapContextMode } : {}),
27+
...(params.bootstrapContextRunKind
28+
? { bootstrapContextRunKind: params.bootstrapContextRunKind }
29+
: {}),
2430
authProfileStore: {
2531
version: 1,
2632
profiles: Object.fromEntries(
@@ -80,6 +86,53 @@ describe("Codex app-server native code mode config", () => {
8086
"features.code_mode_only": true,
8187
});
8288
});
89+
90+
it("disables native Codex project docs for lightweight context threads", () => {
91+
const request = buildThreadStartParams(
92+
createAttemptParams({
93+
provider: "openai",
94+
bootstrapContextMode: "lightweight",
95+
bootstrapContextRunKind: "cron",
96+
}),
97+
{
98+
cwd: "/repo",
99+
dynamicTools: [],
100+
appServer: createAppServerOptions() as never,
101+
developerInstructions: "test instructions",
102+
config: {
103+
project_doc_max_bytes: 64_000,
104+
"features.codex_hooks": true,
105+
},
106+
},
107+
);
108+
109+
expect(request.config).toEqual({
110+
project_doc_max_bytes: 0,
111+
"features.codex_hooks": true,
112+
"features.code_mode": true,
113+
"features.code_mode_only": true,
114+
});
115+
});
116+
117+
it("keeps native Codex project docs enabled when context is not lightweight", () => {
118+
const request = buildThreadResumeParams(
119+
createAttemptParams({ provider: "openai", bootstrapContextRunKind: "cron" }),
120+
{
121+
threadId: "thread-1",
122+
appServer: createAppServerOptions() as never,
123+
developerInstructions: "test instructions",
124+
config: {
125+
project_doc_max_bytes: 64_000,
126+
},
127+
},
128+
);
129+
130+
expect(request.config).toEqual({
131+
project_doc_max_bytes: 64_000,
132+
"features.code_mode": true,
133+
"features.code_mode_only": true,
134+
});
135+
});
83136
});
84137

85138
describe("Codex app-server model provider selection", () => {

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export const CODEX_CODE_MODE_THREAD_CONFIG: JsonObject = {
6565
"features.code_mode_only": true,
6666
};
6767

68+
const CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG: JsonObject = {
69+
project_doc_max_bytes: 0,
70+
};
71+
6872
export async function startOrResumeThread(params: {
6973
client: CodexAppServerClient;
7074
params: EmbeddedRunAttemptParams;
@@ -472,7 +476,7 @@ export function buildThreadStartParams(
472476
sandbox: options.appServer.sandbox,
473477
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
474478
serviceName: "OpenClaw",
475-
config: buildCodexRuntimeThreadConfig(options.config),
479+
config: buildCodexRuntimeThreadConfigForRun(params, options.config),
476480
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
477481
dynamicTools: options.dynamicTools,
478482
experimentalRawEvents: true,
@@ -505,16 +509,31 @@ export function buildThreadResumeParams(
505509
approvalsReviewer: options.appServer.approvalsReviewer,
506510
sandbox: options.appServer.sandbox,
507511
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
508-
config: buildCodexRuntimeThreadConfig(options.config),
512+
config: buildCodexRuntimeThreadConfigForRun(params, options.config),
509513
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
510514
persistExtendedHistory: true,
511515
};
512516
}
513517

514518
export function buildCodexRuntimeThreadConfig(config: JsonObject | undefined): JsonObject {
519+
const runtimeConfig = mergeCodexThreadConfigs(config, CODEX_CODE_MODE_THREAD_CONFIG) ?? {
520+
...CODEX_CODE_MODE_THREAD_CONFIG,
521+
};
522+
return runtimeConfig;
523+
}
524+
525+
function buildCodexRuntimeThreadConfigForRun(
526+
params: EmbeddedRunAttemptParams,
527+
config: JsonObject | undefined,
528+
): JsonObject {
529+
const runtimeConfig = buildCodexRuntimeThreadConfig(config);
530+
if (params.bootstrapContextMode !== "lightweight") {
531+
return runtimeConfig;
532+
}
515533
return (
516-
mergeCodexThreadConfigs(config, CODEX_CODE_MODE_THREAD_CONFIG) ?? {
517-
...CODEX_CODE_MODE_THREAD_CONFIG,
534+
mergeCodexThreadConfigs(runtimeConfig, CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG) ?? {
535+
...runtimeConfig,
536+
...CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG,
518537
}
519538
);
520539
}

src/cron/isolated-agent/run-executor.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export function createCronPromptExecutor(params: {
152152
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
153153
params.cronSession.sessionEntry.systemPromptReport,
154154
);
155+
const bootstrapContextMode = resolveCronBootstrapContextMode(params.agentPayload);
155156

156157
const runPrompt = async (promptText: string) => {
157158
const fallbackResult = await runWithModelFallback({
@@ -202,10 +203,10 @@ export function createCronPromptExecutor(params: {
202203
abortSignal: params.abortSignal,
203204
onExecutionStarted: params.onExecutionStarted,
204205
onExecutionPhase: params.onExecutionPhase,
206+
bootstrapContextMode,
207+
bootstrapContextRunKind: "cron",
205208
bootstrapPromptWarningSignaturesSeen,
206209
bootstrapPromptWarningSignature,
207-
bootstrapContextMode: resolveCronBootstrapContextMode(params.agentPayload),
208-
bootstrapContextRunKind: "cron",
209210
senderIsOwner: params.senderIsOwner,
210211
});
211212
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
@@ -260,7 +261,7 @@ export function createCronPromptExecutor(params: {
260261
verboseLevel: params.resolvedVerboseLevel,
261262
timeoutMs: params.timeoutMs,
262263
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
263-
bootstrapContextMode: resolveCronBootstrapContextMode(params.agentPayload),
264+
bootstrapContextMode,
264265
bootstrapContextRunKind: "cron",
265266
toolsAllow: params.agentPayload?.toolsAllow,
266267
execOverrides: params.suppressExecNotifyOnExit

src/cron/isolated-agent/run.session-key-isolation.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
4141
const result = await runCronIsolatedAgentTurn(
4242
makeIsolatedAgentTurnParams({
4343
sessionKey: "cron:daily-monitor",
44+
job: makeIsolatedAgentTurnJob({
45+
payload: {
46+
kind: "agentTurn",
47+
message: "test",
48+
lightContext: true,
49+
},
50+
}),
4451
}),
4552
);
4653

@@ -56,10 +63,14 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
5663
const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as {
5764
sessionId?: string;
5865
sessionKey?: string;
66+
bootstrapContextMode?: string;
67+
bootstrapContextRunKind?: string;
5968
};
6069
expect(runRequest.sessionId).toBe("isolated-run-1");
6170
expect(runRequest.sessionKey).toBe("agent:default:cron:daily-monitor:run:isolated-run-1");
6271
expect(runRequest.sessionKey).not.toBe("agent:default:cron:daily-monitor");
72+
expect(runRequest.bootstrapContextMode).toBe("lightweight");
73+
expect(runRequest.bootstrapContextRunKind).toBe("cron");
6374
});
6475

6576
it("keeps explicit session-bound cron execution on the requested session key", async () => {
@@ -88,9 +99,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
8899
const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as {
89100
sessionId?: string;
90101
sessionKey?: string;
102+
bootstrapContextMode?: string;
103+
bootstrapContextRunKind?: string;
91104
};
92105
expect(runRequest.sessionId).toBe("bound-run-1");
93106
expect(runRequest.sessionKey).toBe("agent:default:project-alpha-monitor");
107+
expect(runRequest.bootstrapContextMode).toBeUndefined();
108+
expect(runRequest.bootstrapContextRunKind).toBe("cron");
94109
});
95110

96111
it("uses a run-scoped key for CLI isolated cron execution", async () => {
@@ -112,6 +127,13 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
112127
const result = await runCronIsolatedAgentTurn(
113128
makeIsolatedAgentTurnParams({
114129
sessionKey: "cron:cli-monitor",
130+
job: makeIsolatedAgentTurnJob({
131+
payload: {
132+
kind: "agentTurn",
133+
message: "test",
134+
lightContext: true,
135+
},
136+
}),
115137
}),
116138
);
117139

@@ -122,11 +144,15 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
122144
sessionId?: string;
123145
sessionKey?: string;
124146
senderIsOwner?: boolean;
147+
bootstrapContextMode?: string;
148+
bootstrapContextRunKind?: string;
125149
};
126150
expect(runRequest.sessionId).toBe("isolated-cli-run-1");
127151
expect(runRequest.sessionKey).toBe("agent:default:cron:cli-monitor:run:isolated-cli-run-1");
128152
expect(runRequest.sessionKey).not.toBe("agent:default:cron:cli-monitor");
129153
expect(runRequest.senderIsOwner).toBe(true);
154+
expect(runRequest.bootstrapContextMode).toBe("lightweight");
155+
expect(runRequest.bootstrapContextRunKind).toBe("cron");
130156
});
131157

132158
it("runs externally sourced CLI hook turns without owner tool authority", async () => {

0 commit comments

Comments
 (0)