Skip to content

Commit 22aa958

Browse files
authored
Merge branch 'main' into fix/thinking_signature_invalid
2 parents 1c84f79 + 68bfaca commit 22aa958

38 files changed

Lines changed: 1369 additions & 214 deletions

.github/workflows/crabbox-hydrate.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,16 @@ jobs:
431431
if ($LASTEXITCODE -ne 0) {
432432
exit $LASTEXITCODE
433433
}
434+
$workspaceNodeModules = Join-Path $workspace "node_modules"
435+
if (Test-Path $workspaceNodeModules) {
436+
$workspaceNodeModulesItem = Get-Item $workspaceNodeModules -Force
437+
if (($workspaceNodeModulesItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) {
438+
throw "workspace node_modules exists and is not a link: $workspaceNodeModules"
439+
}
440+
} else {
441+
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
442+
}
443+
434444
$corepackShimDir = Join-Path $nodeBin "node_modules\corepack\shims"
435445
if (Test-Path $corepackShimDir) {
436446
$env:PNPM_HOME = $corepackShimDir

extensions/codex/src/app-server/auth-bridge.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import type {
2727
import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js";
2828

2929
const CODEX_APP_SERVER_AUTH_PROVIDER = "openai";
30+
const LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER = "codex-cli";
31+
const CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS = [
32+
CODEX_APP_SERVER_AUTH_PROVIDER,
33+
LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER,
34+
];
3035
const OPENAI_PROVIDER = "openai";
3136
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default";
3237
const CODEX_HOME_ENV_VAR = "CODEX_HOME";
@@ -120,7 +125,7 @@ function ensureCodexAppServerAuthProfileStore(params: {
120125
return ensureAuthProfileStore(params.agentDir, {
121126
allowKeychainPrompt: false,
122127
config: params.config,
123-
externalCliProviderIds: [CODEX_APP_SERVER_AUTH_PROVIDER],
128+
externalCliProviderIds: CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS,
124129
...(params.authProfileId ? { externalCliProfileIds: [params.authProfileId] } : {}),
125130
});
126131
}
@@ -599,7 +604,13 @@ async function resolveOAuthCredentialForCodexAppServer(
599604
}
600605

601606
function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean {
602-
return resolveProviderIdForAuth(provider, { config }) === CODEX_APP_SERVER_AUTH_PROVIDER;
607+
const resolvedProvider = resolveProviderIdForAuth(provider, { config });
608+
return (
609+
resolvedProvider === CODEX_APP_SERVER_AUTH_PROVIDER ||
610+
// Older Codex auth profiles stored the CLI runtime id here. The app-server
611+
// login protocol still receives the same externally managed ChatGPT token.
612+
resolvedProvider === LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER
613+
);
603614
}
604615

605616
function isOpenAIApiKeyBackupCredential(

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,52 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
804804
await run;
805805
});
806806

807+
it("keeps mirrored history when an inactive per-turn context-engine binding starts fresh", async () => {
808+
const sessionFile = path.join(tempDir, "session.jsonl");
809+
const workspaceDir = path.join(tempDir, "workspace");
810+
const sessionManager = SessionManager.open(sessionFile);
811+
sessionManager.appendMessage(userMessage("previous per-turn request", 10) as never);
812+
sessionManager.appendMessage(assistantMessage("previous per-turn answer", 11) as never);
813+
await writeCodexAppServerBinding(sessionFile, {
814+
threadId: "thread-per-turn-context",
815+
cwd: workspaceDir,
816+
dynamicToolsFingerprint: "[]",
817+
contextEngine: {
818+
schemaVersion: 1,
819+
engineId: "lossless-claw",
820+
policyFingerprint:
821+
'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
822+
},
823+
});
824+
const harness = createStartedThreadHarness(async (method) => {
825+
if (method === "thread/start") {
826+
return threadStartResult("thread-fresh");
827+
}
828+
if (method === "thread/resume") {
829+
throw new Error("inactive context-engine bindings should start a fresh thread");
830+
}
831+
return undefined;
832+
});
833+
const params = createParams(sessionFile, workspaceDir);
834+
835+
const run = runCodexAppServerAttempt(params);
836+
await harness.waitForMethod("turn/start");
837+
838+
expect(harness.requests.map((request) => request.method)).toEqual([
839+
"thread/start",
840+
"turn/start",
841+
]);
842+
const inputText = getRequestInputText(harness);
843+
expect(inputText).toContain("OpenClaw assembled context for this turn:");
844+
expect(inputText).toContain("previous per-turn request");
845+
expect(inputText).toContain("previous per-turn answer");
846+
expect(inputText).toContain("Current user request:");
847+
expect(inputText).toContain("hello");
848+
849+
await harness.completeTurn("completed", "thread-fresh");
850+
await run;
851+
});
852+
807853
it("starts a fresh Codex thread and reprojects when context-engine epoch changes", async () => {
808854
const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
809855
const sessionFile = path.join(tempDir, "session.jsonl");

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -443,17 +443,26 @@ export async function runCodexAppServerAttempt(
443443

444444
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, sessionAgentId);
445445
preDynamicStartupStages.mark("session-agent");
446+
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
447+
? params.contextEngine
448+
: undefined;
449+
const isInactiveThreadBootstrapBinding = (binding: CodexAppServerThreadBinding | undefined) =>
450+
!activeContextEngine && binding?.contextEngine?.projection?.mode === "thread_bootstrap";
446451
let startupBinding = await readCodexAppServerBinding(params.sessionFile);
447452
preDynamicStartupStages.mark("read-binding");
448453
const startupBindingAuthProfileId = startupBinding?.authProfileId;
454+
const initialStartupBindingHadInactiveThreadBootstrap =
455+
isInactiveThreadBootstrapBinding(startupBinding);
449456
startupBinding = await rotateOversizedCodexAppServerStartupBinding({
450457
binding: startupBinding,
451458
sessionFile: params.sessionFile,
452459
agentDir,
453460
codexHome: appServer.start.env?.CODEX_HOME,
454461
config: params.config,
455-
contextEngineActive: isActiveHarnessContextEngine(params.contextEngine),
462+
contextEngineActive: Boolean(activeContextEngine),
456463
});
464+
const initialInactiveThreadBootstrapBindingForcedFreshStart =
465+
initialStartupBindingHadInactiveThreadBootstrap && !startupBinding?.threadId;
457466
preDynamicStartupStages.mark("rotate-binding");
458467
const startupAuthProfileCandidate =
459468
params.runtimePlan?.auth.forwardedAuthProfileId ??
@@ -520,9 +529,6 @@ export async function runCodexAppServerAttempt(
520529
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
521530
embeddedAgentLog.warn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
522531
}
523-
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
524-
? params.contextEngine
525-
: undefined;
526532
if (activeContextEngine) {
527533
assertContextEngineHostSupport({
528534
contextEngine: activeContextEngine,
@@ -684,6 +690,8 @@ export async function runCodexAppServerAttempt(
684690
let contextEngineProjection: CodexContextEngineThreadBootstrapProjection | undefined;
685691
let precomputedStaleBindingContinuityProjectionApplied = false;
686692
let staleBindingContinuityForcedFreshStart = false;
693+
let inactiveThreadBootstrapBindingForcedFreshStart =
694+
initialInactiveThreadBootstrapBindingForcedFreshStart;
687695
const applyFreshThreadContinuityProjection = () => {
688696
const projection = projectContextEngineAssemblyForCodex({
689697
assembledMessages: historyMessages,
@@ -875,6 +883,10 @@ export async function runCodexAppServerAttempt(
875883
if (activeContextEngine || !binding?.threadId) {
876884
return false;
877885
}
886+
if (isInactiveThreadBootstrapBinding(binding)) {
887+
inactiveThreadBootstrapBindingForcedFreshStart = true;
888+
return false;
889+
}
878890
const projected = applyResumeStaleBindingContinuityProjection(binding);
879891
precomputedStaleBindingContinuityProjectionApplied = projected;
880892
return projected;
@@ -892,6 +904,12 @@ export async function runCodexAppServerAttempt(
892904
if (action === "started" && staleBindingContinuityForcedFreshStart) {
893905
return true;
894906
}
907+
if (action === "started" && inactiveThreadBootstrapBindingForcedFreshStart) {
908+
// A retired thread-bootstrap context engine already forced Codex onto a
909+
// clean native thread; without that engine active, mirrored history would
910+
// re-inject stale bootstrap context as a new user turn.
911+
return false;
912+
}
895913
if (action === "resumed" && binding) {
896914
return applyResumeStaleBindingContinuityProjection(binding);
897915
}
@@ -909,6 +927,7 @@ export async function runCodexAppServerAttempt(
909927
return;
910928
}
911929
const previousThreadId = startupBinding.threadId;
930+
const hadInactiveThreadBootstrapBinding = isInactiveThreadBootstrapBinding(startupBinding);
912931
const projectedTurnTokens = estimateCodexAppServerProjectedTurnTokens({
913932
prompt: codexTurnPromptText,
914933
developerInstructions: buildRenderedCodexDeveloperInstructions(),
@@ -925,7 +944,10 @@ export async function runCodexAppServerAttempt(
925944
if (startupBinding?.threadId) {
926945
return;
927946
}
928-
staleBindingContinuityForcedFreshStart = precomputedStaleBindingContinuityProjectionApplied;
947+
inactiveThreadBootstrapBindingForcedFreshStart = hadInactiveThreadBootstrapBinding;
948+
staleBindingContinuityForcedFreshStart =
949+
precomputedStaleBindingContinuityProjectionApplied &&
950+
!inactiveThreadBootstrapBindingForcedFreshStart;
929951
if (activeContextEngine) {
930952
contextEngineProjection = undefined;
931953
try {

0 commit comments

Comments
 (0)