Skip to content

Commit b990649

Browse files
committed
fix(codex): restore bounded recovery continuity
1 parent f2ace9f commit b990649

2 files changed

Lines changed: 397 additions & 18 deletions

File tree

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

Lines changed: 250 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,12 +1722,16 @@ describe("runCodexAppServerAttempt", () => {
17221722
]);
17231723
});
17241724

1725-
it("does not inject mirrored history when starting Codex without a native thread binding", async () => {
1725+
it("projects bounded continuity when starting Codex without a native thread binding", async () => {
17261726
const sessionFile = path.join(tempDir, "session.jsonl");
17271727
const workspaceDir = path.join(tempDir, "workspace");
17281728
const sessionManager = SessionManager.open(sessionFile);
1729-
sessionManager.appendMessage(userMessage("we are fixing the Opik default project", Date.now()));
1730-
sessionManager.appendMessage(assistantMessage("Opik default project context", Date.now() + 1));
1729+
await sessionManager.appendMessage(
1730+
userMessage("we are fixing the Opik default project", Date.now()),
1731+
);
1732+
await sessionManager.appendMessage(
1733+
assistantMessage("Opik default project context", Date.now() + 1),
1734+
);
17311735
const harness = createStartedThreadHarness();
17321736
const params = createParams(sessionFile, workspaceDir);
17331737
params.prompt = "make the default webpage openclaw";
@@ -1743,14 +1747,52 @@ describe("runCodexAppServerAttempt", () => {
17431747
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
17441748
"";
17451749

1746-
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
1747-
expect(inputText).not.toContain("we are fixing the Opik default project");
1748-
expect(inputText).not.toContain("Opik default project context");
1749-
expect(inputText).not.toContain("Current user request:");
1750+
expect(inputText).toContain("OpenClaw assembled context for this turn:");
1751+
expect(inputText).toContain("we are fixing the Opik default project");
1752+
expect(inputText).toContain("Opik default project context");
1753+
expect(inputText).toContain("Current user request:");
17501754
expect(inputText).toContain("make the default webpage openclaw");
17511755
});
17521756

1753-
it("does not inject newer mirrored history when resuming an existing Codex thread binding", async () => {
1757+
it("keeps thread-start developer instructions stable when adding fresh-thread continuity", async () => {
1758+
let hookCalls = 0;
1759+
const beforePromptBuild = vi.fn(async () => {
1760+
hookCalls += 1;
1761+
return {
1762+
systemPrompt: `custom codex system ${hookCalls}`,
1763+
prependContext: `queued context ${hookCalls}`,
1764+
};
1765+
});
1766+
initializeGlobalHookRunner(
1767+
createMockPluginRegistry([{ hookName: "before_prompt_build", handler: beforePromptBuild }]),
1768+
);
1769+
const sessionFile = path.join(tempDir, "session.jsonl");
1770+
const workspaceDir = path.join(tempDir, "workspace");
1771+
const sessionManager = SessionManager.open(sessionFile);
1772+
await sessionManager.appendMessage(userMessage("prior visible context", Date.now()));
1773+
await sessionManager.appendMessage(assistantMessage("prior assistant context", Date.now() + 1));
1774+
const harness = createStartedThreadHarness();
1775+
1776+
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
1777+
await harness.waitForMethod("turn/start");
1778+
await new Promise<void>((resolve) => setImmediate(resolve));
1779+
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
1780+
await run;
1781+
1782+
expect(beforePromptBuild).toHaveBeenCalled();
1783+
const threadStart = harness.requests.find((request) => request.method === "thread/start");
1784+
const threadStartParams = threadStart?.params as { developerInstructions?: string } | undefined;
1785+
expect(threadStartParams?.developerInstructions).toContain("custom codex system 1");
1786+
expect(threadStartParams?.developerInstructions).not.toContain("custom codex system 2");
1787+
const turnStart = harness.requests.find((request) => request.method === "turn/start");
1788+
const inputText =
1789+
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
1790+
"";
1791+
expect(inputText).toContain("queued context");
1792+
expect(inputText).toContain("prior visible context");
1793+
});
1794+
1795+
it("does not replay mirrored history already covered by an existing Codex binding", async () => {
17541796
const sessionFile = path.join(tempDir, "session.jsonl");
17551797
const workspaceDir = path.join(tempDir, "workspace");
17561798
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
@@ -1760,11 +1802,11 @@ describe("runCodexAppServerAttempt", () => {
17601802
throw new Error("expected valid Codex binding timestamp");
17611803
}
17621804
const sessionManager = SessionManager.open(sessionFile);
1763-
sessionManager.appendMessage(
1764-
userMessage("we were discussing the Sonnet leak screenshots", bindingUpdatedAt + 1_000),
1805+
await sessionManager.appendMessage(
1806+
userMessage("we were discussing the Sonnet leak screenshots", bindingUpdatedAt - 2_000),
17651807
);
1766-
sessionManager.appendMessage(
1767-
assistantMessage("David Ondrej was mentioned in that prior thread", bindingUpdatedAt + 2_000),
1808+
await sessionManager.appendMessage(
1809+
assistantMessage("David Ondrej was mentioned in that prior thread", bindingUpdatedAt - 1_000),
17681810
);
17691811
const harness = createResumeHarness();
17701812
const params = createParams(sessionFile, workspaceDir);
@@ -1789,6 +1831,132 @@ describe("runCodexAppServerAttempt", () => {
17891831
expect(inputText).toContain("is the previous message trustworthy?");
17901832
});
17911833

1834+
it("projects only newer visible history when a resumed Codex binding is stale", async () => {
1835+
const sessionFile = path.join(tempDir, "session.jsonl");
1836+
const workspaceDir = path.join(tempDir, "workspace");
1837+
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
1838+
const binding = await readCodexAppServerBinding(sessionFile);
1839+
const bindingUpdatedAt = Date.parse(binding?.updatedAt ?? "");
1840+
if (!Number.isFinite(bindingUpdatedAt)) {
1841+
throw new Error("expected valid Codex binding timestamp");
1842+
}
1843+
const sessionManager = SessionManager.open(sessionFile);
1844+
await sessionManager.appendMessage(
1845+
userMessage("old native-owned context", bindingUpdatedAt - 2_000),
1846+
);
1847+
await sessionManager.appendMessage(
1848+
userMessage("we were discussing the Sonnet leak screenshots", bindingUpdatedAt + 1_000),
1849+
);
1850+
await sessionManager.appendMessage(
1851+
assistantMessage("David Ondrej was mentioned in that prior thread", bindingUpdatedAt + 2_000),
1852+
);
1853+
await sessionManager.appendMessage({
1854+
...assistantMessage("copilot mirror context also matters", bindingUpdatedAt + 3_000),
1855+
__openclaw: { mirrorIdentity: "copilot:assistant-1" },
1856+
});
1857+
const harness = createResumeHarness();
1858+
const params = createParams(sessionFile, workspaceDir);
1859+
params.prompt = "is the previous message trustworthy?";
1860+
1861+
const run = runCodexAppServerAttempt(params);
1862+
await harness.waitForMethod("turn/start");
1863+
await new Promise<void>((resolve) => setImmediate(resolve));
1864+
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
1865+
await run;
1866+
1867+
expect(harness.requests.map((request) => request.method)).toContain("thread/resume");
1868+
const turnStart = harness.requests.find((request) => request.method === "turn/start");
1869+
const inputText =
1870+
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
1871+
"";
1872+
1873+
expect(inputText).toContain("OpenClaw assembled context for this turn:");
1874+
expect(inputText).not.toContain("old native-owned context");
1875+
expect(inputText).toContain("we were discussing the Sonnet leak screenshots");
1876+
expect(inputText).toContain("David Ondrej was mentioned in that prior thread");
1877+
expect(inputText).toContain("copilot mirror context also matters");
1878+
expect(inputText).toContain("Current user request:");
1879+
expect(inputText).toContain("is the previous message trustworthy?");
1880+
});
1881+
1882+
it("does not project Codex mirrored transcript echoes as stale binding continuity", async () => {
1883+
const sessionFile = path.join(tempDir, "session.jsonl");
1884+
const workspaceDir = path.join(tempDir, "workspace");
1885+
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
1886+
const binding = await readCodexAppServerBinding(sessionFile);
1887+
const bindingUpdatedAt = Date.parse(binding?.updatedAt ?? "");
1888+
if (!Number.isFinite(bindingUpdatedAt)) {
1889+
throw new Error("expected valid Codex binding timestamp");
1890+
}
1891+
const sessionManager = SessionManager.open(sessionFile);
1892+
await sessionManager.appendMessage({
1893+
...userMessage("codex mirrored user echo", bindingUpdatedAt + 1_000),
1894+
idempotencyKey: "codex-app-server:user-1",
1895+
});
1896+
await sessionManager.appendMessage({
1897+
...assistantMessage("codex mirrored assistant echo", bindingUpdatedAt + 2_000),
1898+
__openclaw: { mirrorIdentity: "codex-app-server:assistant-1" },
1899+
});
1900+
const harness = createResumeHarness();
1901+
const params = createParams(sessionFile, workspaceDir);
1902+
params.prompt = "continue from the real user message";
1903+
1904+
const run = runCodexAppServerAttempt(params);
1905+
await harness.waitForMethod("turn/start");
1906+
await new Promise<void>((resolve) => setImmediate(resolve));
1907+
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
1908+
await run;
1909+
1910+
const turnStart = harness.requests.find((request) => request.method === "turn/start");
1911+
const inputText =
1912+
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
1913+
"";
1914+
1915+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
1916+
expect(inputText).not.toContain("codex mirrored user echo");
1917+
expect(inputText).not.toContain("codex mirrored assistant echo");
1918+
expect(inputText).toContain("continue from the real user message");
1919+
});
1920+
1921+
it("does not replay messages persisted during an active native Codex turn", async () => {
1922+
const sessionFile = path.join(tempDir, "session.jsonl");
1923+
const workspaceDir = path.join(tempDir, "workspace");
1924+
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
1925+
const originalBindingUpdatedAt = Date.now() - 60_000;
1926+
const bindingPath = `${sessionFile}.codex-app-server.json`;
1927+
const bindingPayload = JSON.parse(await fs.readFile(bindingPath, "utf8")) as Record<
1928+
string,
1929+
unknown
1930+
>;
1931+
bindingPayload.updatedAt = new Date(originalBindingUpdatedAt).toISOString();
1932+
await fs.writeFile(bindingPath, `${JSON.stringify(bindingPayload, null, 2)}\n`);
1933+
const sessionManager = SessionManager.open(sessionFile);
1934+
const firstHarness = createResumeHarness();
1935+
const firstRun = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
1936+
await firstHarness.waitForMethod("turn/start");
1937+
await sessionManager.appendMessage(userMessage("steered into active native turn", Date.now()));
1938+
await firstHarness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
1939+
await firstRun;
1940+
const completedBinding = await readCodexAppServerBinding(sessionFile);
1941+
expect(Date.parse(completedBinding?.updatedAt ?? "")).toBeGreaterThan(originalBindingUpdatedAt);
1942+
1943+
const secondHarness = createResumeHarness();
1944+
const secondParams = createParams(sessionFile, workspaceDir);
1945+
secondParams.prompt = "continue after steering";
1946+
const secondRun = runCodexAppServerAttempt(secondParams);
1947+
await secondHarness.waitForMethod("turn/start");
1948+
await secondHarness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
1949+
await secondRun;
1950+
1951+
const turnStart = secondHarness.requests.find((request) => request.method === "turn/start");
1952+
const inputText =
1953+
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
1954+
"";
1955+
expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
1956+
expect(inputText).not.toContain("steered into active native turn");
1957+
expect(inputText).toContain("continue after steering");
1958+
});
1959+
17921960
it("does not project mirrored messages on consecutive resumes", async () => {
17931961
const sessionFile = path.join(tempDir, "session.jsonl");
17941962
const workspaceDir = path.join(tempDir, "workspace");
@@ -1802,10 +1970,10 @@ describe("runCodexAppServerAttempt", () => {
18021970
bindingPayload.updatedAt = new Date(oldBindingUpdatedAt).toISOString();
18031971
await fs.writeFile(bindingPath, `${JSON.stringify(bindingPayload, null, 2)}\n`);
18041972
const sessionManager = SessionManager.open(sessionFile);
1805-
sessionManager.appendMessage(
1973+
await sessionManager.appendMessage(
18061974
userMessage("we were discussing the Sonnet leak screenshots", oldBindingUpdatedAt + 1_000),
18071975
);
1808-
sessionManager.appendMessage(
1976+
await sessionManager.appendMessage(
18091977
assistantMessage(
18101978
"David Ondrej was mentioned in that prior thread",
18111979
oldBindingUpdatedAt + 2_000,
@@ -1824,8 +1992,8 @@ describe("runCodexAppServerAttempt", () => {
18241992
const firstInputText =
18251993
(firstTurnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]
18261994
?.text ?? "";
1827-
expect(firstInputText).not.toContain("OpenClaw assembled context for this turn:");
1828-
expect(firstInputText).not.toContain("we were discussing the Sonnet leak screenshots");
1995+
expect(firstInputText).toContain("OpenClaw assembled context for this turn:");
1996+
expect(firstInputText).toContain("we were discussing the Sonnet leak screenshots");
18291997
expect(firstInputText).toContain("is the previous message trustworthy?");
18301998

18311999
const secondHarness = createResumeHarness();
@@ -3869,6 +4037,72 @@ describe("runCodexAppServerAttempt", () => {
38694037
expect(savedBinding?.threadId).toBe("thread-1");
38704038
});
38714039

4040+
it("preserves stale-binding continuity when token pressure forces a fresh Codex thread", async () => {
4041+
const sessionFile = path.join(tempDir, "session.jsonl");
4042+
const workspaceDir = path.join(tempDir, "workspace");
4043+
const agentDir = path.join(tempDir, "agent");
4044+
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
4045+
const binding = await readCodexAppServerBinding(sessionFile);
4046+
const bindingUpdatedAt = Date.parse(binding?.updatedAt ?? "");
4047+
if (!Number.isFinite(bindingUpdatedAt)) {
4048+
throw new Error("expected valid Codex binding timestamp");
4049+
}
4050+
const sessionManager = SessionManager.open(sessionFile);
4051+
await sessionManager.appendMessage(
4052+
userMessage("post-binding user context", bindingUpdatedAt + 1_000),
4053+
);
4054+
await sessionManager.appendMessage(
4055+
assistantMessage("post-binding assistant context", bindingUpdatedAt + 2_000),
4056+
);
4057+
await fs.writeFile(
4058+
path.join(path.dirname(sessionFile), "sessions.json"),
4059+
JSON.stringify({
4060+
"agent:main:session-1": {
4061+
sessionFile,
4062+
totalTokens: 12_000,
4063+
},
4064+
}),
4065+
);
4066+
const rolloutDir = path.join(agentDir, "codex-home", "sessions");
4067+
await fs.mkdir(rolloutDir, { recursive: true });
4068+
await fs.writeFile(
4069+
path.join(rolloutDir, "rollout-thread-existing.jsonl"),
4070+
`${JSON.stringify({
4071+
payload: {
4072+
type: "token_count",
4073+
info: {
4074+
last_token_usage: {
4075+
total_tokens: 220_000,
4076+
},
4077+
model_context_window: 258_400,
4078+
},
4079+
},
4080+
})}\n`,
4081+
);
4082+
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();
4083+
const params = createParams(sessionFile, workspaceDir);
4084+
params.agentDir = agentDir;
4085+
params.prompt = "large prompt ".repeat(12_000);
4086+
4087+
const run = runCodexAppServerAttempt(params, {
4088+
pluginConfig: { appServer: { mode: "yolo" } },
4089+
});
4090+
await waitForMethod("turn/start");
4091+
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
4092+
await run;
4093+
4094+
expect(requests.map((entry) => entry.method)).toContain("thread/start");
4095+
expect(requests.map((entry) => entry.method)).not.toContain("thread/resume");
4096+
const turnStart = requests.find((request) => request.method === "turn/start");
4097+
const inputText =
4098+
(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
4099+
"";
4100+
expect(inputText).toContain("post-binding user context");
4101+
expect(inputText).toContain("post-binding assistant context");
4102+
const savedBinding = await readCodexAppServerBinding(sessionFile);
4103+
expect(savedBinding?.threadId).toBe("thread-1");
4104+
});
4105+
38724106
it("preserves bound auth when rotating a fallback-fuse native rollout", async () => {
38734107
const sessionFile = path.join(tempDir, "session.jsonl");
38744108
const workspaceDir = path.join(tempDir, "workspace");

0 commit comments

Comments
 (0)