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
+5
View File
@@ -1032,6 +1032,10 @@ Object.assign(CodemanApp.prototype, {
const enabledItems = config.generatedPlan?.filter(i => i.enabled);
try {
const envOverrides = this.buildEnvOverrides(
this.getCaseSettings(config.caseName),
this.loadAppSettingsFromStorage()
);
const res = await fetch('/api/ralph-loop/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1042,6 +1046,7 @@ Object.assign(CodemanApp.prototype, {
maxIterations: config.maxIterations || null,
enableRespawn: config.enableRespawn,
planItems: enabledItems?.length ? enabledItems : undefined,
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
}),
});
const data = await res.json();
+19 -4
View File
@@ -12,6 +12,22 @@
*/
Object.assign(CodemanApp.prototype, {
/**
* Build envOverrides payload from case + global settings.
* Single source of truth for the server-side tmux setenv values.
* Keys omitted when value is default/falsy — backend treats unset as "no override".
*/
buildEnvOverrides(caseSettings, globalSettings) {
const env = {};
if (caseSettings?.agentTeams || globalSettings?.agentTeamsEnabled) {
env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
}
if (globalSettings?.thinkingEffort) {
env.CLAUDE_CODE_EFFORT_LEVEL = globalSettings.thinkingEffort;
}
return env;
},
// ═══════════════════════════════════════════════════════════════
// Quick Start
// ═══════════════════════════════════════════════════════════════
@@ -319,10 +335,7 @@ Object.assign(CodemanApp.prototype, {
// Build env overrides from global + case settings (case overrides global)
const caseSettings = this.getCaseSettings(caseName);
const globalSettings = this.loadAppSettingsFromStorage();
const envOverrides = {};
if (caseSettings.agentTeams || globalSettings.agentTeamsEnabled) {
envOverrides.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
}
const envOverrides = this.buildEnvOverrides(caseSettings, globalSettings);
const hasEnvOverrides = Object.keys(envOverrides).length > 0;
const useOpus1m = caseSettings.opusContext1m || globalSettings.opusContext1mEnabled;
const modelOverride = useOpus1m ? 'opus[1m]' : '';
@@ -526,6 +539,7 @@ Object.assign(CodemanApp.prototype, {
}
// Quick-start with opencode mode (auto-allow tools by default)
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
const res = await fetch('/api/quick-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -533,6 +547,7 @@ Object.assign(CodemanApp.prototype, {
caseName,
mode: 'opencode',
openCodeConfig: { autoAllowTools: true },
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
})
});
const data = await res.json();
+12 -2
View File
@@ -953,11 +953,21 @@ Object.assign(CodemanApp.prototype, {
}
const name = `w${startNumber}-${dirName}`;
// Create session with resumeSessionId
// Create session with resumeSessionId — include envOverrides so resumed
// conversations inherit current UI settings (effort, agent teams, etc.).
// Match by path (not basename) so linked/renamed cases still resolve correctly.
const matchingCase = (this.cases || []).find((c) => c.path === workingDir);
const caseName = matchingCase?.name || workingDir.split('/').pop() || '';
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
const createRes = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workingDir, name, resumeSessionId: sessionId }),
body: JSON.stringify({
workingDir,
name,
resumeSessionId: sessionId,
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
}),
});
const createData = await createRes.json();
if (!createData.success) throw new Error(createData.error);