diff --git a/src/ai-repo-commander.user.js b/src/ai-repo-commander.user.js index dc70a7f..33712a4 100644 --- a/src/ai-repo-commander.user.js +++ b/src/ai-repo-commander.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name AI Repo Commander // @namespace http://tampermonkey.net/ -// @version 1.3.2 -// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, optional auto-submit, and a built-in debug console +// @version 1.3.4 +// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, optional auto-submit, and a built-in debug console (safe manual-retry only) // @author Your Name // @match https://chat.openai.com/* // @match https://chatgpt.com/* @@ -27,7 +27,7 @@ DEBUG_SHOW_PANEL: true, // Show floating debug console UI DEBOUNCE_DELAY: 5000, // Bot typing protection MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries) - VERSION: '1.3.2', + VERSION: '1.3.4', PROCESS_EXISTING: false, // If false, only process *new* messages (no initial rescan) ASSISTANT_ONLY: true, // Process assistant messages by default (core use case) @@ -61,6 +61,16 @@ this.loopCounts = new Map(); this.startedAt = Date.now(); this.panel = null; + + // loop-counts periodic cleanup to avoid unbounded growth + this.loopCleanupInterval = setInterval(() => { + if (Date.now() - this.startedAt > this.cfg.DEBUG_WATCH_MS * 2) { + this.loopCounts.clear(); + this.startedAt = Date.now(); + this.verbose('Cleaned loop counters'); + } + }, this.cfg.DEBUG_WATCH_MS); + if (cfg.DEBUG_SHOW_PANEL) this.mount(); this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`); } @@ -229,6 +239,11 @@ while (body.children.length > this.cfg.DEBUG_MAX_LINES) body.firstChild.remove(); body.scrollTop = body.scrollHeight; } + + destroy() { + try { clearInterval(this.loopCleanupInterval); } catch {} + if (this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel); + } } // ---------------------- Platform selectors ---------------------- @@ -303,6 +318,11 @@ db[this._hash(text)] = Date.now(); this._save(db); } + unmark(text) { // manual retry only + const db = this._load(); + const k = this._hash(text); + if (k in db) { delete db[k]; this._save(db); } + } cleanup() { const db = this._load(); const now = Date.now(); @@ -819,7 +839,7 @@ }); this.observer.observe(document.body, { childList: true, subtree: true }); - // Respect PROCESS_EXISTING on initial scan + // Respect PROCESS_EXISTING on initial scan (explicitly log skip) if (CONFIG.PROCESS_EXISTING) { setTimeout(() => { RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)'); @@ -925,7 +945,19 @@ const message = this.trackedMessages.get(messageId); if (!message) { RC_DEBUG?.error('Message not found', { messageId }); return; } - let parsed = CommandParser.parseYAMLCommand(message.originalText); + // Parsing wrapped for clearer error classification + let parsed; + try { + parsed = CommandParser.parseYAMLCommand(message.originalText); + } catch (err) { + RC_DEBUG?.error(`Command parsing failed: ${err.message}`, { messageId }); + this.updateState(messageId, COMMAND_STATES.ERROR); + // Ignore UI error for common partial/invalid cases + if (/No valid command block|Missing required field:|YAML parsing error/i.test(err.message)) return; + UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message }); + return; + } + this.updateState(messageId, COMMAND_STATES.VALIDATING); let validation = CommandParser.validateStructure(parsed); @@ -951,21 +983,29 @@ if (!validation.isValid) throw new Error(`Final validation failed: ${validation.errors.join(', ')}`); } - // **HARDENED**: pre-mark to avoid duplicate runs if DOM churns + // Pre-mark to avoid duplicate runs if DOM churns — stays marked even on failure this.history.mark(message.originalText); this.updateState(messageId, COMMAND_STATES.EXECUTING); - await ExecutionManager.executeCommand(parsed, message.element); + const result = await ExecutionManager.executeCommand(parsed, message.element); + + // If execution reported failure, mark ERROR but do not unmark (no auto-retries). + if (!result || result.success === false) { + RC_DEBUG?.warn('Execution reported failure; command remains marked (no auto-retry)', { messageId }); + this.updateState(messageId, COMMAND_STATES.ERROR); + return; + } const duration = Date.now() - started; - if (duration < 100) RC_DEBUG?.warn('Command completed suspiciously fast', { messageId, duration }); - if (duration > 30000) RC_DEBUG?.warn('Command took unusually long', { messageId, duration }); + if (duration < 50) RC_DEBUG?.warn('Command completed very fast', { messageId, duration }); + if (duration > 60000) RC_DEBUG?.warn('Command took very long', { messageId, duration }); this.updateState(messageId, COMMAND_STATES.COMPLETE); } catch (error) { const duration = Date.now() - started; RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration }); + // DO NOT unmark — failed commands remain marked to prevent surprise retries this.updateState(messageId, COMMAND_STATES.ERROR); const message = this.trackedMessages.get(messageId); // Silent ignore for non-commands/partials to avoid noisy inline errors @@ -998,6 +1038,7 @@ clearInterval(this.cleanupIntervalId); this.cleanupIntervalId = null; } + RC_DEBUG?.destroy?.(); } setupEmergencyStop() { @@ -1025,6 +1066,32 @@ } } + // ---------------------- Manual retry helpers ---------------------- + let commandMonitor; // forward ref + + window.AI_REPO_RETRY_COMMAND_TEXT = (text) => { + try { + commandMonitor?.history?.unmark?.(text); + RC_DEBUG?.info('Command unmarked for manual retry (by text)', { preview: String(text).slice(0,120) }); + } catch (e) { + RC_DEBUG?.error('Failed to unmark command by text', { error: String(e) }); + } + }; + + window.AI_REPO_RETRY_MESSAGE = (messageId) => { + try { + const msg = commandMonitor?.trackedMessages?.get(messageId); + if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; } + commandMonitor.history.unmark(msg.originalText); + RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId }); + // re-run directly (ignores PROCESS_EXISTING and aiRcProcessed flag) + commandMonitor.updateState(messageId, COMMAND_STATES.PARSING); + commandMonitor.processCommand(messageId); + } catch (e) { + RC_DEBUG?.error('Failed to retry message', { messageId, error: String(e) }); + } + }; + // ---------------------- Test commands (unchanged) ---------------------- const TEST_COMMANDS = { validUpdate: @@ -1063,7 +1130,6 @@ path: . }; // ---------------------- Init ---------------------- - let commandMonitor; function initializeRepoCommander() { if (!RC_DEBUG) RC_DEBUG = new DebugConsole(CONFIG);