Skip to content

Commit e2be8eb

Browse files
committed
fix(codex): stop injecting mirrored history into prompts
1 parent bba8015 commit e2be8eb

6 files changed

Lines changed: 39 additions & 153 deletions

File tree

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

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
4-
import type {
5-
AgentMessage,
6-
EmbeddedRunAttemptParams,
7-
} from "openclaw/plugin-sdk/agent-harness-runtime";
4+
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
85
import { describe, expect, it } from "vitest";
96
import {
107
buildCodexWorkspaceBootstrapContext,
118
buildCodexSystemPromptReport,
129
readContextEngineThreadBootstrapProjection,
1310
remapCodexContextFilePath,
1411
resolveContextEngineBootstrapProjectionDecision,
15-
shouldProjectMirroredHistoryForCodexStart,
1612
} from "./attempt-context.js";
1713
import type { CodexDynamicToolSpec } from "./protocol.js";
1814
import type { CodexAppServerContextEngineBinding } from "./session-binding.js";
@@ -140,44 +136,6 @@ describe("Codex app-server attempt context", () => {
140136
});
141137
});
142138

143-
it("projects mirrored history for transient native-disabled Codex threads", () => {
144-
expect(
145-
shouldProjectMirroredHistoryForCodexStart({
146-
startupBinding: {
147-
threadId: "thread-existing",
148-
dynamicToolsFingerprint: "same-tools",
149-
} as never,
150-
dynamicToolsFingerprint: "same-tools",
151-
historyMessages: [
152-
{
153-
role: "user",
154-
content: "earlier request",
155-
timestamp: Date.now(),
156-
} as AgentMessage,
157-
],
158-
forceProject: true,
159-
}),
160-
).toBe(true);
161-
162-
expect(
163-
shouldProjectMirroredHistoryForCodexStart({
164-
startupBinding: {
165-
threadId: "thread-existing",
166-
dynamicToolsFingerprint: "same-tools",
167-
} as never,
168-
dynamicToolsFingerprint: "same-tools",
169-
historyMessages: [
170-
{
171-
role: "assistant",
172-
content: "earlier response",
173-
timestamp: Date.now(),
174-
} as unknown as AgentMessage,
175-
],
176-
forceProject: true,
177-
}),
178-
).toBe(false);
179-
});
180-
181139
it("reads and compares thread-bootstrap context-engine projections", () => {
182140
const projection = readContextEngineThreadBootstrapProjection({
183141
mode: "thread_bootstrap",

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

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -80,73 +80,6 @@ export async function readMirroredSessionHistoryMessages(
8080
return messages;
8181
}
8282

83-
export function shouldProjectMirroredHistoryForCodexStart(params: {
84-
startupBinding: CodexAppServerThreadBinding | undefined;
85-
dynamicToolsFingerprint: string;
86-
historyMessages: AgentMessage[];
87-
forceProject?: boolean;
88-
}): boolean {
89-
if (!params.historyMessages.some((message) => message.role === "user")) {
90-
return false;
91-
}
92-
if (params.forceProject) {
93-
return true;
94-
}
95-
if (!params.startupBinding?.threadId) {
96-
return true;
97-
}
98-
if (
99-
hasUserVisibleHistoryAfterCodexBinding({
100-
startupBinding: params.startupBinding,
101-
historyMessages: params.historyMessages,
102-
})
103-
) {
104-
return true;
105-
}
106-
return !areCodexDynamicToolFingerprintsCompatible({
107-
previous: params.startupBinding.dynamicToolsFingerprint,
108-
next: params.dynamicToolsFingerprint,
109-
});
110-
}
111-
112-
export function hasUserVisibleHistoryAfterCodexBinding(params: {
113-
startupBinding: CodexAppServerThreadBinding;
114-
historyMessages: AgentMessage[];
115-
}): boolean {
116-
const bindingUpdatedAt = Date.parse(params.startupBinding.updatedAt);
117-
if (!Number.isFinite(bindingUpdatedAt)) {
118-
return false;
119-
}
120-
return params.historyMessages.some((message) => {
121-
if (message.role !== "user" && message.role !== "assistant") {
122-
return false;
123-
}
124-
if (isCodexAppServerMirroredTranscriptMessage(message)) {
125-
return false;
126-
}
127-
const timestamp =
128-
typeof message.timestamp === "number"
129-
? message.timestamp
130-
: typeof message.timestamp === "string"
131-
? Date.parse(message.timestamp)
132-
: Number.NaN;
133-
return Number.isFinite(timestamp) && timestamp > bindingUpdatedAt;
134-
});
135-
}
136-
137-
export function isCodexAppServerMirroredTranscriptMessage(message: AgentMessage): boolean {
138-
const record = message as unknown as Record<string, unknown>;
139-
const idempotencyKey = record.idempotencyKey;
140-
if (typeof idempotencyKey === "string" && idempotencyKey.startsWith("codex-app-server:")) {
141-
return true;
142-
}
143-
const meta = record["__openclaw"];
144-
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
145-
return false;
146-
}
147-
return typeof (meta as Record<string, unknown>).mirrorIdentity === "string";
148-
}
149-
15083
export function readContextEngineThreadBootstrapProjection(
15184
projection: ContextEngineProjection | undefined,
15285
): CodexContextEngineThreadBootstrapProjection | undefined {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
649649
},
650650
);
651651

652-
it("projects mirrored history when an oversized thread-bootstrap binding has no active context engine", async () => {
652+
it("does not inject mirrored history when a stale thread-bootstrap binding has no active context engine", async () => {
653653
const sessionFile = path.join(tempDir, "session.jsonl");
654654
const workspaceDir = path.join(tempDir, "workspace");
655655
const agentDir = path.join(tempDir, "agent");
@@ -730,10 +730,10 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
730730
"turn/start",
731731
]);
732732
const inputText = getRequestInputText(harness);
733-
expect(inputText).toContain("OpenClaw assembled context for this turn:");
734-
expect(inputText).toContain("previous stale-bootstrap request");
735-
expect(inputText).toContain("previous stale-bootstrap answer");
736-
expect(inputText).toContain("Current user request:");
733+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
734+
expect(inputText).not.toContain("previous stale-bootstrap request");
735+
expect(inputText).not.toContain("previous stale-bootstrap answer");
736+
expect(inputText).not.toContain("Current user request:");
737737
expect(inputText).toContain("hello");
738738

739739
await harness.completeTurn("completed", "thread-fresh");

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe("runCodexAppServerAttempt hooks and model diagnostics", () => {
8787
expect(llmInputPayload.model).toBe("gpt-5.4-codex");
8888
expect(llmInputPayload.prompt).toBe("hello");
8989
expect(llmInputPayload.imagesCount).toBe(0);
90-
expect(llmInputPayload.historyMessages?.[0]?.role).toBe("assistant");
90+
expect(llmInputPayload.historyMessages).toEqual([]);
9191
expect(llmInputPayload.systemPrompt).toContain(
9292
"You are a personal agent running inside OpenClaw.",
9393
);
@@ -241,6 +241,8 @@ describe("runCodexAppServerAttempt hooks and model diagnostics", () => {
241241
return {};
242242
});
243243
const params = createParams(sessionFile, workspaceDir);
244+
const sessionManager = SessionManager.open(sessionFile);
245+
sessionManager.appendMessage(assistantMessage("existing context", Date.now()));
244246
params.runtimePlan = createCodexRuntimePlanFixture();
245247
params.config = {
246248
diagnostics: {
@@ -283,6 +285,7 @@ describe("runCodexAppServerAttempt hooks and model diagnostics", () => {
283285
expect(JSON.stringify(startedEvent)).not.toContain("hello");
284286
const startedContent = diagnosticContentByType.get("model.call.started")?.modelContent;
285287
expect(JSON.stringify(startedContent?.inputMessages)).toContain("hello");
288+
expect(JSON.stringify(startedContent?.inputMessages)).not.toContain("existing context");
286289
expect(startedContent?.systemPrompt).toContain(
287290
"You are a personal agent running inside OpenClaw.",
288291
);

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,7 +1692,7 @@ describe("runCodexAppServerAttempt", () => {
16921692
{ runId?: string; sessionId?: string },
16931693
];
16941694
expect(hookInput.prompt).toBe("hello");
1695-
expect(hookInput.messages?.[0]?.role).toBe("assistant");
1695+
expect(hookInput.messages).toEqual([]);
16961696
expect(hookContext.runId).toBe("run-1");
16971697
expect(hookContext.sessionId).toBe("session-1");
16981698
const threadStart = harness.requests.find((request) => request.method === "thread/start");
@@ -1711,7 +1711,7 @@ describe("runCodexAppServerAttempt", () => {
17111711
]);
17121712
});
17131713

1714-
it("projects mirrored history when starting Codex without a native thread binding", async () => {
1714+
it("does not inject mirrored history when starting Codex without a native thread binding", async () => {
17151715
const sessionFile = path.join(tempDir, "session.jsonl");
17161716
const workspaceDir = path.join(tempDir, "workspace");
17171717
const sessionManager = SessionManager.open(sessionFile);
@@ -1732,14 +1732,14 @@ describe("runCodexAppServerAttempt", () => {
17321732
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
17331733
"";
17341734

1735-
expect(inputText).toContain("OpenClaw assembled context for this turn:");
1736-
expect(inputText).toContain("we are fixing the Opik default project");
1737-
expect(inputText).toContain("Opik default project context");
1738-
expect(inputText).toContain("Current user request:");
1735+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
1736+
expect(inputText).not.toContain("we are fixing the Opik default project");
1737+
expect(inputText).not.toContain("Opik default project context");
1738+
expect(inputText).not.toContain("Current user request:");
17391739
expect(inputText).toContain("make the default webpage openclaw");
17401740
});
17411741

1742-
it("projects newer mirrored history when resuming an existing Codex thread binding", async () => {
1742+
it("does not inject newer mirrored history when resuming an existing Codex thread binding", async () => {
17431743
const sessionFile = path.join(tempDir, "session.jsonl");
17441744
const workspaceDir = path.join(tempDir, "workspace");
17451745
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
@@ -1771,14 +1771,14 @@ describe("runCodexAppServerAttempt", () => {
17711771
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
17721772
"";
17731773

1774-
expect(inputText).toContain("OpenClaw assembled context for this turn:");
1775-
expect(inputText).toContain("we were discussing the Sonnet leak screenshots");
1776-
expect(inputText).toContain("David Ondrej was mentioned in that prior thread");
1777-
expect(inputText).toContain("Current user request:");
1774+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
1775+
expect(inputText).not.toContain("we were discussing the Sonnet leak screenshots");
1776+
expect(inputText).not.toContain("David Ondrej was mentioned in that prior thread");
1777+
expect(inputText).not.toContain("Current user request:");
17781778
expect(inputText).toContain("is the previous message trustworthy?");
17791779
});
17801780

1781-
it("does not reproject Codex-owned mirrored messages on consecutive resumes", async () => {
1781+
it("does not project mirrored messages on consecutive resumes", async () => {
17821782
const sessionFile = path.join(tempDir, "session.jsonl");
17831783
const workspaceDir = path.join(tempDir, "workspace");
17841784
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
@@ -1813,8 +1813,8 @@ describe("runCodexAppServerAttempt", () => {
18131813
const firstInputText =
18141814
(firstTurnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]
18151815
?.text ?? "";
1816-
expect(firstInputText).toContain("OpenClaw assembled context for this turn:");
1817-
expect(firstInputText).toContain("we were discussing the Sonnet leak screenshots");
1816+
expect(firstInputText).not.toContain("OpenClaw assembled context for this turn:");
1817+
expect(firstInputText).not.toContain("we were discussing the Sonnet leak screenshots");
18181818
expect(firstInputText).toContain("is the previous message trustworthy?");
18191819

18201820
const secondHarness = createResumeHarness();

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

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import {
6363
readMirroredSessionHistoryMessages,
6464
renderCodexWorkspaceMemoryReference,
6565
resolveContextEngineBootstrapProjectionDecision,
66-
shouldProjectMirroredHistoryForCodexStart,
6766
} from "./attempt-context.js";
6867
import {
6968
classifyCodexModelCallFailureKind,
@@ -756,27 +755,16 @@ export async function runCodexAppServerAttempt(
756755
error: formatErrorMessage(assembleErr),
757756
});
758757
}
759-
} else if (
760-
shouldProjectMirroredHistoryForCodexStart({
761-
startupBinding,
762-
dynamicToolsFingerprint: codexDynamicToolsFingerprint(toolBridge.specs),
763-
historyMessages,
764-
forceProject: !nativeToolSurfaceEnabled,
765-
})
766-
) {
767-
const projection = projectContextEngineAssemblyForCodex({
768-
assembledMessages: historyMessages,
769-
originalHistoryMessages: historyMessages,
770-
prompt: params.prompt,
771-
});
772-
promptText = projection.promptText;
773-
prePromptMessageCount = projection.prePromptMessageCount;
774758
}
759+
// Codex app-server threads own conversation continuity. The mirrored
760+
// OpenClaw transcript is persistence/search state; only explicit
761+
// non-legacy context engines project alternate context into the turn prompt.
762+
const codexModelInputHistoryMessages = activeContextEngine ? historyMessages : [];
775763
const buildPromptFromCurrentInputs = () =>
776764
resolveAgentHarnessBeforePromptBuildResult({
777765
prompt: prependCurrentInboundContext(promptText, params.currentInboundContext),
778766
developerInstructions,
779-
messages: historyMessages,
767+
messages: codexModelInputHistoryMessages,
780768
ctx: hookContext,
781769
});
782770
let promptBuild = await buildPromptFromCurrentInputs();
@@ -1584,12 +1572,12 @@ export async function runCodexAppServerAttempt(
15841572
model: params.modelId,
15851573
systemPrompt: buildRenderedCodexDeveloperInstructions(),
15861574
prompt: codexTurnPromptText,
1587-
historyMessages,
1575+
historyMessages: codexModelInputHistoryMessages,
15881576
imagesCount: params.images?.length ?? 0,
15891577
tools,
15901578
});
1591-
const buildTurnStartFailureMessages = () => [
1592-
...historyMessages,
1579+
const buildCodexModelInputMessages = () => [
1580+
...codexModelInputHistoryMessages,
15931581
buildCodexUserPromptMessage({ ...params, prompt: codexTurnPromptText }),
15941582
];
15951583
const codexModelCallBaseFields = {
@@ -1608,7 +1596,7 @@ export async function runCodexAppServerAttempt(
16081596
baseFields: codexModelCallBaseFields,
16091597
capture: codexModelContentCapture,
16101598
tools,
1611-
buildInputMessages: buildTurnStartFailureMessages,
1599+
buildInputMessages: buildCodexModelInputMessages,
16121600
buildSystemPrompt: buildRenderedCodexDeveloperInstructions,
16131601
onErrorDiagnostic: (error) => {
16141602
embeddedAgentLog.debug("codex app-server model call diagnostic ended with error", {
@@ -1748,9 +1736,13 @@ export async function runCodexAppServerAttempt(
17481736
turnStartErrorMessage,
17491737
turnStartFailureKind ? { failureKind: turnStartFailureKind } : {},
17501738
);
1739+
const turnStartFailureMessages = [
1740+
...historyMessages,
1741+
buildCodexUserPromptMessage({ ...params, prompt: codexTurnPromptText }),
1742+
];
17511743
await runCodexAgentEndHook(params, {
17521744
event: {
1753-
messages: buildTurnStartFailureMessages(),
1745+
messages: turnStartFailureMessages,
17541746
success: false,
17551747
error: turnStartErrorMessage,
17561748
durationMs: Date.now() - attemptStartedAt,
@@ -1790,7 +1782,7 @@ export async function runCodexAppServerAttempt(
17901782
...buildCodexTurnStartFailureResult({
17911783
params,
17921784
message: usageLimitError.message,
1793-
messagesSnapshot: buildTurnStartFailureMessages(),
1785+
messagesSnapshot: turnStartFailureMessages,
17941786
systemPromptReport,
17951787
}),
17961788
};

0 commit comments

Comments
 (0)