Skip to content

Commit 3f166b1

Browse files
committed
fix(codex): bound hook-expanded prompts
1 parent 8e89055 commit 3f166b1

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

extensions/codex/src/app-server/context-engine-projection.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,22 @@ describe("projectContextEngineAssemblyForCodex", () => {
279279
expect(fitted).not.toContain("hook context");
280280
});
281281

282+
it("keeps the original input when a hook appends context without a projection", () => {
283+
const prompt = "current prompt survives";
284+
const hookAppend = `\n\nhook context ${"h".repeat(800)}`;
285+
const maxChars = 420;
286+
287+
const fitted = fitCodexProjectedContextForTurnStart({
288+
promptText: `${prompt}${hookAppend}`,
289+
preservedRange: { start: 0, end: prompt.length },
290+
maxChars,
291+
});
292+
293+
expect(fitted.length).toBeLessThanOrEqual(maxChars);
294+
expect(fitted).toContain(prompt);
295+
expect(fitted).not.toContain("hook context");
296+
});
297+
282298
it("bounds output for a large request under the default Codex turn limit", () => {
283299
const maxChars = CODEX_TURN_START_TEXT_INPUT_MAX_CHARS;
284300
// A large assembled header prefix already over the cap forces the

extensions/codex/src/app-server/context-engine-projection.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export function fitCodexProjectedContextForTurnStart(params: {
122122
promptText: string;
123123
contextRange?: CodexProjectedContextRange;
124124
requestRange?: CodexProjectedContextRange;
125+
preservedRange?: CodexProjectedContextRange;
125126
maxChars?: number;
126127
}): string {
127128
const maxChars =
@@ -133,7 +134,19 @@ export function fitCodexProjectedContextForTurnStart(params: {
133134
}
134135
const range = normalizeProjectedContextRange(params.contextRange, params.promptText.length);
135136
if (!range) {
136-
return params.promptText;
137+
const preservedRange = normalizeProjectedContextRange(
138+
params.preservedRange,
139+
params.promptText.length,
140+
);
141+
if (!preservedRange) {
142+
return params.promptText;
143+
}
144+
const preservedText = params.promptText.slice(preservedRange.start, preservedRange.end);
145+
if (preservedText.length >= maxChars) {
146+
return truncateOlderContext(preservedText, maxChars);
147+
}
148+
const beforeRange = params.promptText.slice(0, preservedRange.start);
149+
return `${truncateOlderContext(beforeRange, maxChars - preservedText.length)}${preservedText}`;
137150
}
138151

139152
const beforeContext = params.promptText.slice(0, range.start);

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,33 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
540540
await run;
541541
});
542542

543+
it("bounds hook-appended prompts without an active context engine", async () => {
544+
initializeGlobalHookRunner(
545+
createMockPluginRegistry([
546+
{
547+
hookName: "before_prompt_build",
548+
handler: async () => ({ appendContext: `hook context ${"h".repeat(1_100_000)}` }),
549+
},
550+
]),
551+
);
552+
const sessionFile = path.join(tempDir, "session.jsonl");
553+
const workspaceDir = path.join(tempDir, "workspace");
554+
const harness = createStartedThreadHarness();
555+
const params = createParams(sessionFile, workspaceDir);
556+
params.prompt = "current prompt survives";
557+
558+
const run = runCodexAppServerAttempt(params);
559+
await harness.waitForMethod("turn/start");
560+
561+
const inputText = getRequestInputText(harness);
562+
expect(inputText.length).toBeLessThanOrEqual(CODEX_TURN_START_TEXT_INPUT_MAX_CHARS);
563+
expect(inputText).toContain("current prompt survives");
564+
expect(inputText).not.toContain("hook context");
565+
566+
await harness.completeTurn();
567+
await run;
568+
});
569+
543570
it("uses configured compaction reserve when sizing Codex context-engine projections", async () => {
544571
const sessionFile = path.join(tempDir, "session.jsonl");
545572
const workspaceDir = path.join(tempDir, "workspace");

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,26 @@ export async function runCodexAppServerAttempt(
10281028
? { beforeAgentStartResult: params.beforeAgentStartResult }
10291029
: {}),
10301030
});
1031+
const resolveShiftedPromptInputRange = (
1032+
prompt: string,
1033+
promptInputRange: { start: number; end: number } | undefined,
1034+
turnPromptText: string,
1035+
): CodexProjectedContextRange | undefined => {
1036+
if (
1037+
!promptInputRange ||
1038+
promptInputRange.start < 0 ||
1039+
promptInputRange.end < promptInputRange.start ||
1040+
promptInputRange.end > prompt.length ||
1041+
!turnPromptText.endsWith(prompt)
1042+
) {
1043+
return undefined;
1044+
}
1045+
const turnPromptOffset = turnPromptText.length - prompt.length;
1046+
return {
1047+
start: turnPromptOffset + promptInputRange.start,
1048+
end: turnPromptOffset + promptInputRange.end,
1049+
};
1050+
};
10311051
const resolveShiftedPromptContextRange = (
10321052
prompt: string,
10331053
promptInputRange: { start: number; end: number } | undefined,
@@ -1095,10 +1115,16 @@ export async function runCodexAppServerAttempt(
10951115
promptBuild.promptInputRange,
10961116
turnPromptText,
10971117
);
1118+
const preservedRange = resolveShiftedPromptInputRange(
1119+
promptBuild.prompt,
1120+
promptBuild.promptInputRange,
1121+
turnPromptText,
1122+
);
10981123
return fitCodexProjectedContextForTurnStart({
10991124
promptText: turnPromptText,
11001125
contextRange: projectedRanges?.contextRange,
11011126
requestRange: projectedRanges?.requestRange,
1127+
preservedRange,
11021128
});
11031129
};
11041130
let codexTurnPromptText = decorateCodexTurnPromptText(promptBuild);

0 commit comments

Comments
 (0)