From 63be081741483cf48051d38c77dc7e08329a6959 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 14:30:10 -0800 Subject: [PATCH] fix: renameAlias data corruption, empty sessionId match, NaN threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix renameAlias() leaving orphaned newAlias key on save failure, causing in-memory data corruption with both old and new keys present - Add sessionPath validation to setAlias() to reject empty/null paths - Guard getSessionById() against empty string matching all sessions (startsWith('') is always true in JavaScript) - Fix suggest-compact.js NaN comparison when COMPACT_THRESHOLD env var is set to a non-numeric value — falls back to 50 instead of silently disabling the threshold check - Sync suggest-compact.js to .cursor/ copy --- .../strategic-compact/suggest-compact.js | 74 +++++++++++++++++++ scripts/hooks/suggest-compact.js | 3 +- scripts/lib/session-aliases.js | 8 +- scripts/lib/session-manager.js | 2 +- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 .cursor/skills/strategic-compact/suggest-compact.js diff --git a/.cursor/skills/strategic-compact/suggest-compact.js b/.cursor/skills/strategic-compact/suggest-compact.js new file mode 100644 index 00000000..d17068d9 --- /dev/null +++ b/.cursor/skills/strategic-compact/suggest-compact.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * Strategic Compact Suggester + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs on PreToolUse or periodically to suggest manual compaction at logical intervals + * + * Why manual over auto-compact: + * - Auto-compact happens at arbitrary points, often mid-task + * - Strategic compacting preserves context through logical phases + * - Compact after exploration, before execution + * - Compact after completing a milestone, before starting next + */ + +const fs = require('fs'); +const path = require('path'); +const { + getTempDir, + writeFile, + log +} = require('../lib/utils'); + +async function main() { + // Track tool call count (increment in a temp file) + // Use a session-specific counter file based on session ID from environment + // or parent PID as fallback + const sessionId = process.env.CLAUDE_SESSION_ID || String(process.ppid) || 'default'; + const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`); + const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10); + const threshold = Number.isFinite(rawThreshold) ? rawThreshold : 50; + + let count = 1; + + // Read existing count or start at 1 + // Use fd-based read+write to reduce (but not eliminate) race window + // between concurrent hook invocations + try { + const fd = fs.openSync(counterFile, 'a+'); + try { + const buf = Buffer.alloc(64); + const bytesRead = fs.readSync(fd, buf, 0, 64, 0); + if (bytesRead > 0) { + const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10); + count = Number.isFinite(parsed) ? parsed + 1 : 1; + } + // Truncate and write new value + fs.ftruncateSync(fd, 0); + fs.writeSync(fd, String(count), 0); + } finally { + fs.closeSync(fd); + } + } catch { + // Fallback: just use writeFile if fd operations fail + writeFile(counterFile, String(count)); + } + + // Suggest compact after threshold tool calls + if (count === threshold) { + log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`); + } + + // Suggest at regular intervals after threshold + if (count > threshold && count % 25 === 0) { + log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`); + } + + process.exit(0); +} + +main().catch(err => { + console.error('[StrategicCompact] Error:', err.message); + process.exit(0); +}); diff --git a/scripts/hooks/suggest-compact.js b/scripts/hooks/suggest-compact.js index 87a33fe4..d17068d9 100644 --- a/scripts/hooks/suggest-compact.js +++ b/scripts/hooks/suggest-compact.js @@ -27,7 +27,8 @@ async function main() { // or parent PID as fallback const sessionId = process.env.CLAUDE_SESSION_ID || String(process.ppid) || 'default'; const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`); - const threshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10); + const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10); + const threshold = Number.isFinite(rawThreshold) ? rawThreshold : 50; let count = 1; diff --git a/scripts/lib/session-aliases.js b/scripts/lib/session-aliases.js index af721bbc..4d14de30 100644 --- a/scripts/lib/session-aliases.js +++ b/scripts/lib/session-aliases.js @@ -189,6 +189,11 @@ function setAlias(alias, sessionPath, title = null) { return { success: false, error: 'Alias name cannot be empty' }; } + // Validate session path + if (!sessionPath || typeof sessionPath !== 'string' || sessionPath.trim().length === 0) { + return { success: false, error: 'Session path cannot be empty' }; + } + if (!/^[a-zA-Z0-9_-]+$/.test(alias)) { return { success: false, error: 'Alias name must contain only letters, numbers, dashes, and underscores' }; } @@ -325,8 +330,9 @@ function renameAlias(oldAlias, newAlias) { }; } - // Restore old alias on failure + // Restore old alias and remove new alias on failure data.aliases[oldAlias] = aliasData; + delete data.aliases[newAlias]; return { success: false, error: 'Failed to rename alias' }; } diff --git a/scripts/lib/session-manager.js b/scripts/lib/session-manager.js index deeae2c5..5d78a965 100644 --- a/scripts/lib/session-manager.js +++ b/scripts/lib/session-manager.js @@ -271,7 +271,7 @@ function getSessionById(sessionId, includeContent = false) { if (!metadata) continue; // Check if session ID matches (short ID or full filename without .tmp) - const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(sessionId); + const shortIdMatch = sessionId.length > 0 && metadata.shortId !== 'no-id' && metadata.shortId.startsWith(sessionId); const filenameMatch = filename === sessionId || filename === `${sessionId}.tmp`; const noIdMatch = metadata.shortId === 'no-id' && filename === `${sessionId}-session.tmp`;