fix: cap session-start context injection

This commit is contained in:
Affaan Mustafa
2026-04-30 08:31:48 -04:00
committed by Affaan Mustafa
parent 95bef977c1
commit b1456bd954
4 changed files with 200 additions and 59 deletions

View File

@@ -428,6 +428,12 @@ export ECC_HOOK_PROFILE=standard
# Comma-separated hook IDs to disable # Comma-separated hook IDs to disable
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck" export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
# Cap SessionStart additional context (default: 8000 chars)
export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely for low-context/local-model setups
export ECC_SESSION_START_CONTEXT=off
``` ```
--- ---
@@ -1043,7 +1049,7 @@ Official references:
<details> <details>
<summary><b>My context window is shrinking / Claude is running out of context</b></summary> <summary><b>My context window is shrinking / Claude is running out of context</b></summary>
Too many MCP servers eat your context. Each MCP tool description consumes tokens from your 200k window, potentially reducing it to ~70k. Too many MCP servers eat your context. Each MCP tool description consumes tokens from your 200k window, potentially reducing it to ~70k. SessionStart context is capped at 8000 characters by default; lower it with `ECC_SESSION_START_MAX_CHARS=4000` or disable it with `ECC_SESSION_START_CONTEXT=off` for local-model or low-context setups.
**Fix:** Disable unused MCPs from Claude Code with `/mcp`. Claude Code writes those runtime choices to `~/.claude.json`; `.claude/settings.json` and `.claude/settings.local.json` are not reliable toggles for already-loaded MCP servers. **Fix:** Disable unused MCPs from Claude Code with `/mcp`. Claude Code writes those runtime choices to `~/.claude.json`; `.claude/settings.json` and `.claude/settings.local.json` are not reliable toggles for already-loaded MCP servers.

View File

@@ -98,6 +98,12 @@ export ECC_HOOK_PROFILE=standard
# Disable specific hook IDs (comma-separated) # Disable specific hook IDs (comma-separated)
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck" export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
# Cap SessionStart additional context (default: 8000 chars)
export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely
export ECC_SESSION_START_CONTEXT=off
``` ```
Profiles: Profiles:

View File

@@ -32,6 +32,7 @@ const INSTINCT_CONFIDENCE_THRESHOLD = 0.7;
const MAX_INJECTED_INSTINCTS = 6; const MAX_INJECTED_INSTINCTS = 6;
const MAX_INJECTED_LEARNED_SKILLS = 6; const MAX_INJECTED_LEARNED_SKILLS = 6;
const MAX_LEARNED_SKILL_SUMMARY_CHARS = 220; const MAX_LEARNED_SKILL_SUMMARY_CHARS = 220;
const DEFAULT_SESSION_START_CONTEXT_MAX_CHARS = 8000;
const DEFAULT_SESSION_RETENTION_DAYS = 30; const DEFAULT_SESSION_RETENTION_DAYS = 30;
/** /**
@@ -88,6 +89,33 @@ function getSessionRetentionDays() {
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SESSION_RETENTION_DAYS; return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SESSION_RETENTION_DAYS;
} }
function isSessionStartContextDisabled() {
const raw = String(process.env.ECC_SESSION_START_CONTEXT || '').trim().toLowerCase();
return ['0', 'false', 'off', 'none', 'disabled'].includes(raw);
}
function getSessionStartMaxContextChars() {
const raw = process.env.ECC_SESSION_START_MAX_CHARS;
if (!raw) return DEFAULT_SESSION_START_CONTEXT_MAX_CHARS;
const parsed = Number.parseInt(raw, 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_SESSION_START_CONTEXT_MAX_CHARS;
}
function limitSessionStartContext(additionalContext, maxChars = getSessionStartMaxContextChars()) {
const context = String(additionalContext || '');
if (context.length <= maxChars) {
return context;
}
const marker = '\n\n[SessionStart truncated context. Set ECC_SESSION_START_MAX_CHARS to raise the cap or ECC_SESSION_START_CONTEXT=off to disable injected context.]';
const prefixLength = Math.max(0, maxChars - marker.length);
log(`[SessionStart] Truncated additional context from ${context.length} to ${maxChars} chars`);
return `${context.slice(0, prefixLength).trimEnd()}${marker}`.slice(0, maxChars);
}
function pruneExpiredSessions(searchDirs, retentionDays) { function pruneExpiredSessions(searchDirs, retentionDays) {
const uniqueDirs = Array.from(new Set(searchDirs.filter(dir => typeof dir === 'string' && dir.length > 0))); const uniqueDirs = Array.from(new Set(searchDirs.filter(dir => typeof dir === 'string' && dir.length > 0)));
let removed = 0; let removed = 0;
@@ -468,6 +496,9 @@ async function main() {
const learnedDir = getLearnedSkillsDir(); const learnedDir = getLearnedSkillsDir();
const additionalContextParts = []; const additionalContextParts = [];
const observerContext = resolveProjectContext(); const observerContext = resolveProjectContext();
const maxContextChars = getSessionStartMaxContextChars();
const explicitContextDisabled = isSessionStartContextDisabled();
const shouldInjectContext = !explicitContextDisabled && maxContextChars !== 0;
// Ensure directories exist // Ensure directories exist
ensureDir(sessionsDir); ensureDir(sessionsDir);
@@ -490,6 +521,13 @@ async function main() {
log('[SessionStart] No CLAUDE_SESSION_ID available; skipping observer lease registration'); log('[SessionStart] No CLAUDE_SESSION_ID available; skipping observer lease registration');
} }
if (explicitContextDisabled) {
log('[SessionStart] Additional context injection disabled by ECC_SESSION_START_CONTEXT');
} else if (maxContextChars === 0) {
log('[SessionStart] Additional context injection disabled by ECC_SESSION_START_MAX_CHARS=0');
}
if (shouldInjectContext) {
const instinctSummary = summarizeActiveInstincts(observerContext); const instinctSummary = summarizeActiveInstincts(observerContext);
if (instinctSummary) { if (instinctSummary) {
additionalContextParts.push(instinctSummary); additionalContextParts.push(instinctSummary);
@@ -553,6 +591,7 @@ async function main() {
if (learnedSkillSummary) { if (learnedSkillSummary) {
additionalContextParts.push(learnedSkillSummary); additionalContextParts.push(learnedSkillSummary);
} }
}
// Check for available session aliases // Check for available session aliases
const aliases = listAliases({ limit: 5 }); const aliases = listAliases({ limit: 5 });
@@ -584,12 +623,17 @@ async function main() {
parts.push(`frameworks: ${projectInfo.frameworks.join(', ')}`); parts.push(`frameworks: ${projectInfo.frameworks.join(', ')}`);
} }
log(`[SessionStart] Project detected — ${parts.join('; ')}`); log(`[SessionStart] Project detected — ${parts.join('; ')}`);
if (shouldInjectContext) {
additionalContextParts.push(`Project type: ${JSON.stringify(projectInfo)}`); additionalContextParts.push(`Project type: ${JSON.stringify(projectInfo)}`);
}
} else { } else {
log('[SessionStart] No specific project type detected'); log('[SessionStart] No specific project type detected');
} }
await writeSessionStartPayload(additionalContextParts.join('\n\n')); const additionalContext = shouldInjectContext
? limitSessionStartContext(additionalContextParts.join('\n\n'), maxContextChars)
: '';
await writeSessionStartPayload(additionalContext);
} }
function writeSessionStartPayload(additionalContext) { function writeSessionStartPayload(additionalContext) {

View File

@@ -447,6 +447,91 @@ async function runTests() {
passed++; passed++;
else failed++; else failed++;
if (
await asyncTest('caps very large session-start context by default', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-large-start-${Date.now()}`);
const sessionsDir = getLegacySessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
const sessionFile = path.join(sessionsDir, '2026-02-11-large000-session.tmp');
fs.writeFileSync(sessionFile, `# Large Session\n\nSTART_MARKER\n${'A'.repeat(20000)}\nEND_MARKER\n`);
try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome
});
assert.strictEqual(result.code, 0);
const additionalContext = getSessionStartAdditionalContext(result.stdout);
assert.ok(additionalContext.length <= 8200, `context should stay near the 8000-char default cap, got ${additionalContext.length}`);
assert.ok(additionalContext.includes('START_MARKER'), 'Should keep the start of the selected session summary');
assert.ok(additionalContext.includes('[SessionStart truncated'), 'Should explain that context was truncated');
assert.ok(!additionalContext.includes('END_MARKER'), 'Should not inject the full oversized session summary');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
await asyncTest('honors ECC_SESSION_START_MAX_CHARS for injected context', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-max-start-${Date.now()}`);
const sessionsDir = getLegacySessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
const sessionFile = path.join(sessionsDir, '2026-02-11-max0000-session.tmp');
fs.writeFileSync(sessionFile, `# Sized Session\n\n${'B'.repeat(1200)}\n`);
try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
ECC_SESSION_START_MAX_CHARS: '700'
});
assert.strictEqual(result.code, 0);
const additionalContext = getSessionStartAdditionalContext(result.stdout);
assert.ok(additionalContext.length <= 700, `context should respect configured cap, got ${additionalContext.length}`);
assert.ok(additionalContext.includes('[SessionStart truncated'), 'Should include a truncation marker');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if (
await asyncTest('disables session-start additional context when requested', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-disabled-start-${Date.now()}`);
const sessionsDir = getLegacySessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
const sessionFile = path.join(sessionsDir, '2026-02-11-disabled-session.tmp');
fs.writeFileSync(sessionFile, '# Disabled Session\n\nDO_NOT_INJECT_THIS\n');
try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
ECC_SESSION_START_CONTEXT: 'off'
});
assert.strictEqual(result.code, 0);
const additionalContext = getSessionStartAdditionalContext(result.stdout);
assert.strictEqual(additionalContext, '', 'Should emit no additional context when disabled');
assert.ok(result.stderr.includes('Additional context injection disabled'), `Should log disabled mode, stderr: ${result.stderr}`);
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;
if ( if (
await asyncTest('prefers canonical session-data content over legacy duplicates', async () => { await asyncTest('prefers canonical session-data content over legacy duplicates', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-canonical-start-${Date.now()}`); const isoHome = path.join(os.tmpdir(), `ecc-canonical-start-${Date.now()}`);