fix(codex): preserve projected context after hooks

This commit is contained in:
Vincent Koc
2026-06-20 23:45:09 +08:00
committed by Vincent Koc
parent faa4f5a23b
commit 22c5ced69f
2 changed files with 53 additions and 5 deletions
@@ -9,9 +9,15 @@ import {
type HarnessContextEngine as ContextEngine,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
import {
initializeGlobalHookRunner,
resetGlobalHookRunner,
} from "openclaw/plugin-sdk/hook-runtime";
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
import { registerSandboxBackend } from "openclaw/plugin-sdk/sandbox";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CodexAppServerClientFactory } from "./client-factory.js";
import { CODEX_TURN_START_TEXT_INPUT_MAX_CHARS } from "./context-engine-projection.js";
import type { CodexServerNotification } from "./protocol.js";
import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js";
import {
@@ -71,9 +77,7 @@ const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({
web_search: "disabled",
});
function writeCodexAppServerBinding(
...args: Parameters<typeof writeRawCodexAppServerBinding>
) {
function writeCodexAppServerBinding(...args: Parameters<typeof writeRawCodexAppServerBinding>) {
const [sessionFile, binding, lookup] = args;
return writeRawCodexAppServerBinding(
sessionFile,
@@ -349,6 +353,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
afterEach(async () => {
resetCodexAppServerClientFactoryForTest();
resetGlobalHookRunner();
vi.restoreAllMocks();
await fs.rm(tempDir, { recursive: true, force: true });
});
@@ -493,6 +498,44 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
await run;
});
it("bounds active context-engine projections when prompt hooks append context", async () => {
initializeGlobalHookRunner(
createMockPluginRegistry([
{ hookName: "before_prompt_build", handler: async () => ({ appendContext: "hook tail" }) },
]),
);
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const contextEngine = createContextEngine({
assemble: vi.fn(async () => ({
messages: [
assistantMessage(
`older context ${"x".repeat(CODEX_TURN_START_TEXT_INPUT_MAX_CHARS)} recent anchor`,
10,
),
],
estimatedTokens: 300_000,
})),
});
const harness = createStartedThreadHarness();
const params = createParams(sessionFile, workspaceDir);
params.contextEngine = contextEngine;
params.contextTokenBudget = 300_000;
params.prompt = "current prompt survives";
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
const inputText = getRequestInputText(harness);
expect(inputText.length).toBeLessThanOrEqual(CODEX_TURN_START_TEXT_INPUT_MAX_CHARS);
expect(inputText).toContain("recent anchor");
expect(inputText).toContain("current prompt survives");
expect(inputText).toContain("hook tail");
await harness.completeTurn();
await run;
});
it("uses configured compaction reserve when sizing Codex context-engine projections", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
@@ -1028,10 +1028,15 @@ export async function runCodexAppServerAttempt(
prompt: string,
turnPromptText: string,
): CodexProjectedContextRange | undefined => {
if (!promptContextRange || !prompt.endsWith(promptText) || !turnPromptText.endsWith(prompt)) {
if (!promptContextRange || !turnPromptText.endsWith(prompt)) {
return undefined;
}
// Prompt-build hooks can append context after the projected prompt, so it
// is no longer necessarily the suffix of the assembled prompt.
const promptTextOffset = prompt.lastIndexOf(promptText);
if (promptTextOffset === -1) {
return undefined;
}
const promptTextOffset = prompt.length - promptText.length;
const turnPromptOffset = turnPromptText.length - prompt.length + promptTextOffset;
return {
start: turnPromptOffset + promptContextRange.start,