mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 20:54:46 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user