refactor: pass envOverrides via tmux export instead of disk write

CLAUDE_CODE_EFFORT_LEVEL (and any CLAUDE_CODE_* / OPENCODE_* key) now flows:
  UI dropdown → POST /api/sessions { envOverrides }
             → new Session({ envOverrides })
             → this._envOverrides
             → tmux-manager.buildEnvExports appends `export KEY=<shellescape(VALUE)>`

Previously the API wrote envOverrides to <case>/.claude/settings.local.json, which
created stale state (UI dropdown disagreeing with disk) and polluted user project
directories. Now envOverrides are ephemeral spawn-time state, preserved across
respawnPane cycles via this._envOverrides and across server restart via
SessionState.envOverrides in state.json.

Also removes the now-unused updateCaseEnvVars import from session-routes.ts.
This commit is contained in:
Teigen
2026-04-24 09:49:52 +08:00
parent 6280998bd8
commit a1c69f7405
12 changed files with 225 additions and 24 deletions
+9 -5
View File
@@ -14,7 +14,7 @@ import { RespawnController } from '../../respawn-controller.js';
import { RalphConfigSchema, FixPlanImportSchema, RalphPromptWriteSchema, RalphLoopStartSchema } from '../schemas.js';
import { SseEvent } from '../sse-events.js';
import { autoConfigureRalph, CASES_DIR, SETTINGS_PATH, findSessionOrFail, parseBody } from '../route-helpers.js';
import { writeHooksConfig } from '../../hooks-config.js';
import { writeHooksConfig, stripCaseEnvKeys } from '../../hooks-config.js';
import { generateClaudeMd } from '../../templates/claude-md.js';
import { getLifecycleLog } from '../../session-lifecycle-log.js';
import type { SessionPort, EventPort, RespawnPort, ConfigPort, InfraPort } from '../ports/index.js';
@@ -268,10 +268,8 @@ export function registerRalphRoutes(
);
}
const { caseName, taskDescription, completionPhrase, maxIterations, enableRespawn, planItems } = parseBody(
RalphLoopStartSchema,
req.body
);
const { caseName, taskDescription, completionPhrase, maxIterations, enableRespawn, planItems, envOverrides } =
parseBody(RalphLoopStartSchema, req.body);
const casePath = join(CASES_DIR, caseName);
@@ -298,6 +296,11 @@ export function registerRalphRoutes(
}
}
// Strip stale disk entries for keys this request is actively setting.
if (envOverrides && Object.keys(envOverrides).length > 0) {
await stripCaseEnvKeys(casePath, Object.keys(envOverrides));
}
// Create session
const niceConfig = await ctx.getGlobalNiceConfig();
const rlModelConfig = await ctx.getModelConfig();
@@ -311,6 +314,7 @@ export function registerRalphRoutes(
model: rlModelConfig?.defaultModel || undefined,
claudeMode: rlClaudeModeConfig.claudeMode,
allowedTools: rlClaudeModeConfig.allowedTools,
envOverrides,
});
// Configure Ralph tracker
+36 -7
View File
@@ -44,7 +44,7 @@ import {
validatePathWithinBase,
} from '../route-helpers.js';
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
import { writeHooksConfig, updateCaseEnvVars, updateCaseModel } from '../../hooks-config.js';
import { writeHooksConfig, updateCaseModel, stripCaseEnvKeys } from '../../hooks-config.js';
import { generateClaudeMd } from '../../templates/claude-md.js';
import { imageWatcher } from '../../image-watcher.js';
import { getLifecycleLog } from '../../session-lifecycle-log.js';
@@ -166,9 +166,21 @@ export function registerSessionRoutes(
}
}
// Write env overrides to .claude/settings.local.json if provided
if (body.envOverrides && Object.keys(body.envOverrides).length > 0) {
await updateCaseEnvVars(workingDir, body.envOverrides);
// envOverrides flow through Session → tmux setenv (ephemeral, per-session).
//
// For keys the caller is actively setting, strip any stale disk entry a prior
// Codeman version may have written. Scope limited to:
// - Claude mode (OpenCode doesn't read .claude/settings.local.json)
// - workingDir inside CASES_DIR (Codeman's managed territory — we never mutate
// .claude/settings.local.json in arbitrary user repos that POST /api/sessions
// can target, because those may have hand-authored values).
const canStripDisk =
body.mode !== 'opencode' &&
body.envOverrides &&
Object.keys(body.envOverrides).length > 0 &&
workingDir.startsWith(CASES_DIR + '/');
if (canStripDisk) {
await stripCaseEnvKeys(workingDir, Object.keys(body.envOverrides!));
}
// Write model override to .claude/settings.local.json if provided
@@ -239,6 +251,7 @@ export function registerSessionRoutes(
allowedTools: claudeModeConfig.allowedTools,
openCodeConfig: mode === 'opencode' ? body.openCodeConfig : undefined,
resumeSessionId: validatedResumeId,
envOverrides: body.envOverrides,
});
ctx.addSession(session);
@@ -854,7 +867,11 @@ export function registerSessionRoutes(
);
}
const { prompt, workingDir } = parseBody(QuickRunSchema, req.body, 'Invalid request body');
const {
prompt,
workingDir,
envOverrides: runEnvOverrides,
} = parseBody(QuickRunSchema, req.body, 'Invalid request body');
if (!prompt.trim()) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'prompt is required');
@@ -873,7 +890,7 @@ export function registerSessionRoutes(
}
}
const session = new Session({ workingDir: dir });
const session = new Session({ workingDir: dir, envOverrides: runEnvOverrides });
ctx.addSession(session);
ctx.store.incrementSessionsCreated();
ctx.persistSessionState(session);
@@ -910,7 +927,12 @@ export function registerSessionRoutes(
);
}
const { caseName = 'testcase', mode = 'claude', openCodeConfig } = parseBody(QuickStartSchema, req.body);
const {
caseName = 'testcase',
mode = 'claude',
openCodeConfig,
envOverrides,
} = parseBody(QuickStartSchema, req.body);
// Check OpenCode availability if requested
if (mode === 'opencode') {
@@ -962,6 +984,12 @@ export function registerSessionRoutes(
}
}
// Strip stale disk entries for keys this request is actively setting (Claude only —
// see POST /api/sessions for full rationale).
if (mode !== 'opencode' && envOverrides && Object.keys(envOverrides).length > 0) {
await stripCaseEnvKeys(casePath, Object.keys(envOverrides));
}
// Create a new session with the case as working directory
// Apply global Nice priority config and model config from settings
const niceConfig = await ctx.getGlobalNiceConfig();
@@ -983,6 +1011,7 @@ export function registerSessionRoutes(
claudeMode: qsClaudeModeConfig.claudeMode,
allowedTools: qsClaudeModeConfig.allowedTools,
openCodeConfig: mode === 'opencode' ? openCodeConfig : undefined,
envOverrides,
});
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting