diff --git a/scripts/smart-install.js b/scripts/smart-install.js index 74d6e7c1..eb93aa8c 100644 --- a/scripts/smart-install.js +++ b/scripts/smart-install.js @@ -5,7 +5,7 @@ * Ensures Bun runtime and uv (Python package manager) are installed * (auto-installs if missing) and handles dependency installation when needed. */ -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs'; import { execSync, spawnSync } from 'child_process'; import { join } from 'path'; import { homedir } from 'os'; @@ -260,6 +260,15 @@ function installDeps() { console.error('📦 Installing dependencies with Bun...'); + // Clear stale native module cache (sharp/libvips) to prevent broken dylib references. + // Bun's cache can retain native binaries that reference companion libraries at + // broken relative paths after version upgrades. + const bunCacheImgDir = join(homedir(), '.bun', 'install', 'cache', '@img'); + if (existsSync(bunCacheImgDir)) { + console.error(' Clearing stale native module cache (@img/sharp)...'); + rmSync(bunCacheImgDir, { recursive: true, force: true }); + } + // Quote path for Windows paths with spaces const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath; diff --git a/scripts/sync-marketplace.cjs b/scripts/sync-marketplace.cjs index 229ba0f3..923073c0 100644 --- a/scripts/sync-marketplace.cjs +++ b/scripts/sync-marketplace.cjs @@ -29,6 +29,18 @@ function getCurrentBranch() { } } +function getGitignoreExcludes(basePath) { + const gitignorePath = path.join(basePath, '.gitignore'); + if (!existsSync(gitignorePath)) return ''; + + const lines = readFileSync(gitignorePath, 'utf-8').split('\n'); + return lines + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#') && !line.startsWith('!')) + .map(pattern => `--exclude=${JSON.stringify(pattern)}`) + .join(' '); +} + const branch = getCurrentBranch(); const isForce = process.argv.includes('--force'); @@ -60,13 +72,16 @@ function getPluginVersion() { // Normal rsync for main branch or fresh install console.log('Syncing to marketplace...'); try { + const rootDir = path.join(__dirname, '..'); + const gitignoreExcludes = getGitignoreExcludes(rootDir); + execSync( - 'rsync -av --delete --exclude=.git --exclude=/.mcp.json --exclude=bun.lock --exclude=package-lock.json ./ ~/.claude/plugins/marketplaces/thedotmack/', + `rsync -av --delete --exclude=.git --exclude=/.mcp.json --exclude=bun.lock --exclude=package-lock.json ${gitignoreExcludes} ./ ~/.claude/plugins/marketplaces/thedotmack/`, { stdio: 'inherit' } ); // Remove stale lockfiles before install — they pin old native dep versions - const { unlinkSync } = require('fs'); + const { unlinkSync, rmSync } = require('fs'); for (const lockfile of ['package-lock.json', 'bun.lock']) { const lockpath = path.join(INSTALLED_PATH, lockfile); if (existsSync(lockpath)) { @@ -75,6 +90,14 @@ try { } } + // Clear stale native module cache (sharp/libvips) — Bun's cache can retain + // native binaries that reference companion libraries at broken relative paths + const bunCacheImgDir = path.join(os.homedir(), '.bun', 'install', 'cache', '@img'); + if (existsSync(bunCacheImgDir)) { + rmSync(bunCacheImgDir, { recursive: true, force: true }); + console.log('Cleared stale native module cache (@img/sharp)'); + } + console.log('Running npm install in marketplace...'); execSync( 'cd ~/.claude/plugins/marketplaces/thedotmack/ && npm install', @@ -85,9 +108,12 @@ try { const version = getPluginVersion(); const CACHE_VERSION_PATH = path.join(CACHE_BASE_PATH, version); + const pluginDir = path.join(rootDir, 'plugin'); + const pluginGitignoreExcludes = getGitignoreExcludes(pluginDir); + console.log(`Syncing to cache folder (version ${version})...`); execSync( - `rsync -av --delete --exclude=.git plugin/ "${CACHE_VERSION_PATH}/"`, + `rsync -av --delete --exclude=.git ${pluginGitignoreExcludes} plugin/ "${CACHE_VERSION_PATH}/"`, { stdio: 'inherit' } ); @@ -121,4 +147,4 @@ try { } catch (error) { console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message); process.exit(1); -} +} \ No newline at end of file diff --git a/src/services/sqlite/PendingMessageStore.ts b/src/services/sqlite/PendingMessageStore.ts index 8bf19b35..618e1154 100644 --- a/src/services/sqlite/PendingMessageStore.ts +++ b/src/services/sqlite/PendingMessageStore.ts @@ -133,16 +133,27 @@ export class PendingMessageStore { * @param thresholdMs Messages processing longer than this are considered stale (default: 5 minutes) * @returns Number of messages reset */ - resetStaleProcessingMessages(thresholdMs: number = 5 * 60 * 1000): number { + resetStaleProcessingMessages(thresholdMs: number = 5 * 60 * 1000, sessionDbId?: number): number { const cutoff = Date.now() - thresholdMs; - const stmt = this.db.prepare(` - UPDATE pending_messages - SET status = 'pending', started_processing_at_epoch = NULL - WHERE status = 'processing' AND started_processing_at_epoch < ? - `); - const result = stmt.run(cutoff); + let stmt; + let result; + if (sessionDbId !== undefined) { + stmt = this.db.prepare(` + UPDATE pending_messages + SET status = 'pending', started_processing_at_epoch = NULL + WHERE status = 'processing' AND started_processing_at_epoch < ? AND session_db_id = ? + `); + result = stmt.run(cutoff, sessionDbId); + } else { + stmt = this.db.prepare(` + UPDATE pending_messages + SET status = 'pending', started_processing_at_epoch = NULL + WHERE status = 'processing' AND started_processing_at_epoch < ? + `); + result = stmt.run(cutoff); + } if (result.changes > 0) { - logger.info('QUEUE', `RESET_STALE | count=${result.changes} | thresholdMs=${thresholdMs}`); + logger.info('QUEUE', `RESET_STALE | count=${result.changes} | thresholdMs=${thresholdMs}${sessionDbId !== undefined ? ` | sessionDbId=${sessionDbId}` : ''}`); } return result.changes; } diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index a94b5833..6b0b1baf 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -604,6 +604,20 @@ export class WorkerService { return; } + // Idle timeout means no new work arrived for 3 minutes - don't restart + if (session.idleTimedOut) { + logger.info('SYSTEM', 'Generator exited due to idle timeout, not restarting', { + sessionId: session.sessionDbId + }); + // Reset stale processing messages so they can be picked up later + const { PendingMessageStore: PendingMsgStore } = require('./sqlite/PendingMessageStore.js'); + const idlePendingStore = new PendingMsgStore(this.dbManager.getSessionStore().db, 3); + idlePendingStore.resetStaleProcessingMessages(0, session.sessionDbId); // Reset this session's messages only + session.idleTimedOut = false; // Reset flag + this.broadcastProcessingStatus(); + return; + } + // Check if there's pending work that needs processing with a fresh AbortController const { PendingMessageStore } = require('./sqlite/PendingMessageStore.js'); const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3); diff --git a/src/services/worker-types.ts b/src/services/worker-types.ts index f1098f5c..10b850af 100644 --- a/src/services/worker-types.ts +++ b/src/services/worker-types.ts @@ -35,6 +35,7 @@ export interface ActiveSession { currentProvider: 'claude' | 'gemini' | 'openrouter' | null; // Track which provider is currently running consecutiveRestarts: number; // Track consecutive restart attempts to prevent infinite loops forceInit?: boolean; // Force fresh SDK session (skip resume) + idleTimedOut?: boolean; // Set when session exits due to idle timeout (prevents restart loop) // CLAIM-CONFIRM FIX: Track IDs of messages currently being processed // These IDs will be confirmed (deleted) after successful storage processingMessageIds: number[]; diff --git a/src/services/worker/GeminiAgent.ts b/src/services/worker/GeminiAgent.ts index 458f6f7a..8bb69dc4 100644 --- a/src/services/worker/GeminiAgent.ts +++ b/src/services/worker/GeminiAgent.ts @@ -238,17 +238,25 @@ export class GeminiAgent { } // Process response using shared ResponseProcessor - await processAgentResponse( - obsResponse.content || '', - session, - this.dbManager, - this.sessionManager, - worker, - tokensUsed, - originalTimestamp, - 'Gemini', - lastCwd - ); + if (obsResponse.content) { + await processAgentResponse( + obsResponse.content, + session, + this.dbManager, + this.sessionManager, + worker, + tokensUsed, + originalTimestamp, + 'Gemini', + lastCwd + ); + } else { + logger.warn('SDK', 'Empty Gemini observation response, skipping processing to preserve message', { + sessionId: session.sessionDbId, + messageId: session.processingMessageIds[session.processingMessageIds.length - 1] + }); + // Don't confirm - leave message for stale recovery + } } else if (message.type === 'summarize') { // CRITICAL: Check memorySessionId BEFORE making expensive LLM call @@ -280,17 +288,25 @@ export class GeminiAgent { } // Process response using shared ResponseProcessor - await processAgentResponse( - summaryResponse.content || '', - session, - this.dbManager, - this.sessionManager, - worker, - tokensUsed, - originalTimestamp, - 'Gemini', - lastCwd - ); + if (summaryResponse.content) { + await processAgentResponse( + summaryResponse.content, + session, + this.dbManager, + this.sessionManager, + worker, + tokensUsed, + originalTimestamp, + 'Gemini', + lastCwd + ); + } else { + logger.warn('SDK', 'Empty Gemini summary response, skipping processing to preserve message', { + sessionId: session.sessionDbId, + messageId: session.processingMessageIds[session.processingMessageIds.length - 1] + }); + // Don't confirm - leave message for stale recovery + } } } @@ -413,6 +429,7 @@ export class GeminiAgent { 'gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', + 'gemini-3-flash', 'gemini-3-flash-preview', ]; diff --git a/src/services/worker/SessionManager.ts b/src/services/worker/SessionManager.ts index 030a266d..e6bc3986 100644 --- a/src/services/worker/SessionManager.ts +++ b/src/services/worker/SessionManager.ts @@ -423,6 +423,7 @@ export class SessionManager { signal: session.abortController.signal, onIdleTimeout: () => { logger.info('SESSION', 'Triggering abort due to idle timeout to kill subprocess', { sessionDbId }); + session.idleTimedOut = true; session.abortController.abort(); } })) {