#!/usr/bin/env python3 """ LACK v3.9.2 – STACK Edition (Complete Production Release) Fixed: logError defined before first use in embedded Node.js server. Fixed: Empty code block detection + JSON linter (no more "No linter configured for json"). Full code moderation pipeline with /repo, /lint, /moderate commands. Enhanced with small‑model resilience (JSON repair, fallbacks, forced commits). """ import os import sys import subprocess import stat import webbrowser import threading import time import json from pathlib import Path VERSION = "3.9.2" # ---------------------------------------------------------------------- # Embedded Node.js server – REORDERED to avoid ReferenceError on logError # ---------------------------------------------------------------------- SERVER_JS = r''' const express = require('express'); const path = require('path'); const WebSocket = require('ws'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const axios = require('axios'); const cheerio = require('cheerio'); const simpleGit = require('simple-git'); const { exec } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); // ==================== CONFIGURATION (early) ==================== const configPath = path.join(__dirname, 'config', 'lack.config.json'); let config; try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (err) { config = { httpPort: 3721, agents: [ { id: "agent1", name: "Agent 1", model: "qwen2.5:0.5b", systemPrompt: "You are a helpful AI assistant.", channels: ["general","random","siphon","code"] }, { id: "agent2", name: "Agent 2", model: "qwen2.5:0.5b", systemPrompt: "You are a creative AI.", channels: ["general","random","siphon","code"] } ], channels: [ { id: "general", name: "general" }, { id: "random", name: "random" }, { id: "siphon", name: "siphon" }, { id: "code", name: "code" } ], dms: [] }; fs.mkdirSync(path.join(__dirname, 'config'), { recursive: true }); fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } const PORT = config.httpPort || 3721; const OLLAMA_URL = 'http://localhost:11434'; const RESEARCH_DIR = path.join(__dirname, 'research'); const LOG_DIR = path.join(__dirname, 'logs'); const ERROR_LOG_PATH = path.join(LOG_DIR, 'error.log'); fs.mkdirSync(LOG_DIR, { recursive: true }); fs.mkdirSync(path.join(__dirname, 'lineage'), { recursive: true }); const GIT = simpleGit(); // ==================== LOGGING & ERROR HANDLERS (before any usage) ==================== global.errorLog = []; function logError(errorObj) { const entry = { timestamp: Date.now(), ...errorObj }; try { fs.appendFileSync(ERROR_LOG_PATH, JSON.stringify(entry) + '\n'); } catch (e) {} global.errorLog.unshift(entry); if (global.errorLog.length > 200) global.errorLog.pop(); console.error('[ERROR]', entry); } process.on('uncaughtException', (err) => { console.error('🔥 Uncaught Exception:', err); if (typeof logError === 'function') logError({ context: 'uncaughtException', error: err.stack }); }); process.on('unhandledRejection', (reason, promise) => { console.error('❌ Unhandled Rejection:', reason); if (typeof logError === 'function') logError({ context: 'unhandledRejection', error: reason }); }); process.on('SIGTERM', () => { console.log('[LACK] SIGTERM received, shutting down...'); process.exit(0); }); process.on('SIGINT', () => { console.log('[LACK] SIGINT received, shutting down...'); process.exit(0); }); // ==================== STACK CORE (can now safely call logError) ==================== const STACK_ROOT = path.join(__dirname, 'lack_repos'); const TEMPLATES_DIR = path.join(STACK_ROOT, 'templates'); const MANIFEST_PATH = path.join(TEMPLATES_DIR, 'manifest.json'); fs.mkdirSync(STACK_ROOT, { recursive: true }); fs.mkdirSync(TEMPLATES_DIR, { recursive: true }); let stackManifest = {}; async function getEmbedding(text) { try { const res = await axios.post('http://localhost:11434/api/embeddings', { model: 'nomic-embed-text', prompt: text.slice(0, 2000) }); return res.data.embedding; } catch (e) { logError({ context: 'getEmbedding', error: e.message }); return null; } } function cosineSimilarity(v1, v2) { if (!v1 || !v2 || v1.length !== v2.length) return 0; let dot = 0, mag1 = 0, mag2 = 0; for (let i = 0; i < v1.length; i++) { dot += v1[i] * v2[i]; mag1 += v1[i] * v1[i]; mag2 += v2[i] * v2[i]; } return dot / (Math.sqrt(mag1) * Math.sqrt(mag2)); } async function scanAndReindexTemplates() { if (!fs.existsSync(TEMPLATES_DIR)) return; const items = fs.readdirSync(TEMPLATES_DIR, { withFileTypes: true }); const newManifest = {}; for (const item of items) { if (item.isDirectory()) { const templatePath = path.join(TEMPLATES_DIR, item.name); let combinedText = ''; const files = {}; const walk = (dir) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) walk(full); else if (/\.(js|json|md|txt|py|html|css)$/.test(e.name)) { const content = fs.readFileSync(full, 'utf-8'); combinedText += content + '\n'; files[path.relative(templatePath, full)] = content; } } }; walk(templatePath); if (combinedText.length === 0) continue; const vector = await getEmbedding(combinedText); if (vector) newManifest[item.name] = { vector, files }; } } stackManifest = newManifest; fs.writeFileSync(MANIFEST_PATH, JSON.stringify(stackManifest, null, 2)); console.log(`[STACK] Reindexed ${Object.keys(stackManifest).length} templates`); } async function stackBuild(repoName) { const repoPath = path.join(STACK_ROOT, repoName); if (fs.existsSync(repoPath)) return `⚠️ Repository ${repoName} already exists.`; fs.mkdirSync(repoPath, { recursive: true }); await execPromise(`git init && git checkout -b main`, { cwd: repoPath }); const configFile = path.join(repoPath, 'STACK_CONFIG.json'); fs.writeFileSync(configFile, JSON.stringify({ project: repoName, managedBy: "LACK-STACK" }, null, 2)); return `✅ STACK repository **${repoName}** created at ${repoPath}`; } async function stackAdd(intent, storeId) { if (!fs.existsSync(MANIFEST_PATH)) return "No templates found. Add folders to ./lack_repos/templates/ first."; const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')); let queryVec = await getEmbedding(intent); let useFallback = !queryVec; let best = null, bestScore = -1; if (useFallback) { console.warn('[STACK] Embedding failed, using keyword fallback'); const intentWords = intent.toLowerCase().split(/\s+/); for (const [name, data] of Object.entries(manifest)) { const combinedText = Object.values(data.files).join(' ').toLowerCase(); let score = 0; for (const word of intentWords) { if (combinedText.includes(word)) score++; } if (score > bestScore) { bestScore = score; best = { name, files: data.files }; } } if (best && bestScore > 0) { const activeRepo = activeStackRepo.get(storeId) || 'default'; const repoPath = path.join(STACK_ROOT, activeRepo); if (!fs.existsSync(repoPath)) return `No active repository. Use /stack set first.`; for (const [relPath, content] of Object.entries(best.files)) { const target = path.join(repoPath, relPath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content); } await execPromise(`git add . && git commit -m "STACK add: ${best.name}"`, { cwd: repoPath }).catch(e => console.warn(e)); return `🔍 Keyword match: **${best.name}** (score ${bestScore}) – fallback used.\nApplied ${Object.keys(best.files).length} files to ${activeRepo}.`; } else { return `No template matches keywords: "${intent}".`; } } // normal vector search for (const [name, data] of Object.entries(manifest)) { const score = cosineSimilarity(queryVec, data.vector); if (score > bestScore) { bestScore = score; best = { name, files: data.files }; } } if (best && bestScore > 0.45) { const activeRepo = activeStackRepo.get(storeId) || 'default'; const repoPath = path.join(STACK_ROOT, activeRepo); if (!fs.existsSync(repoPath)) return `No active repository. Use /stack set first.`; for (const [relPath, content] of Object.entries(best.files)) { const target = path.join(repoPath, relPath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content); } await execPromise(`git add . && git commit -m "STACK add: ${best.name}"`, { cwd: repoPath }).catch(e => console.warn(e)); return `🔍 Best match: **${best.name}** (score ${bestScore.toFixed(2)})\nApplied ${Object.keys(best.files).length} files to ${activeRepo}.`; } else { return `No strong match for "${intent}" (best score ${bestScore.toFixed(2)}).`; } } async function stackImport(jsonPath) { const fullPath = path.resolve(jsonPath); if (!fs.existsSync(fullPath)) return `File not found: ${fullPath}`; const data = JSON.parse(fs.readFileSync(fullPath, 'utf-8')); if (!data.templates) return "Invalid format: missing 'templates' key."; for (const [name, template] of Object.entries(data.templates)) { const dir = path.join(TEMPLATES_DIR, name); fs.mkdirSync(dir, { recursive: true }); for (const [file, content] of Object.entries(template.files || {})) { const target = path.join(dir, file); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content); } } await scanAndReindexTemplates(); return `Imported ${Object.keys(data.templates).length} templates.`; } let stackWatcher = null; function startStackWatcher() { if (stackWatcher) clearInterval(stackWatcher); stackWatcher = setInterval(async () => { const oldKeys = Object.keys(stackManifest); await scanAndReindexTemplates(); const newKeys = Object.keys(stackManifest); if (JSON.stringify(oldKeys) !== JSON.stringify(newKeys)) console.log('[STACK] Templates changed, reindexed.'); }, 10000); } const activeStackRepo = new Map(); // ==================== HARDENED JSON EXTRACTION (with repair) ==================== function repairJSON(str) { if (!str || typeof str !== 'string') return str; // 1. Remove markdown fences str = str.replace(/```json\s*|```\s*/g, ''); // 2. Add missing quotes around unquoted keys str = str.replace(/(\{|\,)\s*([a-zA-Z0-9_]+)\s*\:/g, '$1"$2":'); // 3. Fix trailing commas str = str.replace(/,\s*\}/g, '}').replace(/,\s*\]/g, ']'); // 4. Replace single quotes with double quotes (but careful inside strings) str = str.replace(/'/g, '"'); // 5. Add missing closing braces/brackets let openBraces = (str.match(/\{/g) || []).length; let closeBraces = (str.match(/\}/g) || []).length; let openBrackets = (str.match(/\[/g) || []).length; let closeBrackets = (str.match(/\]/g) || []).length; str += '}'.repeat(openBraces - closeBraces) + ']'.repeat(openBrackets - closeBrackets); return str; } function extractJSON(str) { str = repairJSON(str); if (!str || typeof str !== 'string') return null; str = str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); const jsonBlock = str.match(/```json\s*([\s\S]*?)\s*```/); if (jsonBlock && jsonBlock[1]) { try { return JSON.parse(jsonBlock[1].trim()); } catch(e) {} } let start = str.indexOf('{'); if (start === -1) return null; let depth = 0, end = -1; for (let i = start; i < str.length; i++) { if (str[i] === '{') depth++; else if (str[i] === '}') { depth--; if (depth === 0) { end = i; break; } } } if (end !== -1) { try { return JSON.parse(str.substring(start, end + 1)); } catch(e) { let fragment = str.substring(start, end + 1); let opens = (fragment.match(/\{/g) || []).length - (fragment.match(/\}/g) || []).length; let arrOpens = (fragment.match(/\[/g) || []).length - (fragment.match(/\]/g) || []).length; fragment += ']'.repeat(Math.max(0, arrOpens)) + '}'.repeat(Math.max(0, opens)); try { return JSON.parse(fragment); } catch(e2) {} } } const possible = str.match(/\{[\s\S]*\}/); if (possible) { try { return JSON.parse(possible[0]); } catch(e) {} } return null; } // ==================== FORCE CODE BLOCKS FROM PLAIN TEXT ==================== function ensureCodeBlock(text, language) { if (text.includes('```')) return text; // Heuristics: looks like code if (text.includes(' f.endsWith('.md')); return files.map(f => fs.readFileSync(path.join(SKILLS_DIR, f), 'utf-8')).join('\n\n---\n\n'); } async function saveSkill(summary, toolCalls) { const content = `# Skill ${new Date().toISOString()}\n\n**Summary:** ${summary}\n**Tools used:** ${toolCalls.map(t => t.name).join(', ')}\n\`\`\`json\n${JSON.stringify(toolCalls, null, 2)}\n\`\`\``; await executeTool('write_file', { path: `.skills/skill_${Date.now()}.md`, content }); } // ==================== GIT HELPERS ==================== async function ensureGitRepo() { try { if (!fs.existsSync(RESEARCH_DIR)) fs.mkdirSync(RESEARCH_DIR, { recursive: true }); const gitDir = path.join(RESEARCH_DIR, '.git'); if (!fs.existsSync(gitDir)) { await GIT.cwd(RESEARCH_DIR).init(); await GIT.cwd(RESEARCH_DIR).addConfig('user.name', 'LACK SIPHON'); await GIT.cwd(RESEARCH_DIR).addConfig('user.email', 'lack@localhost'); await GIT.cwd(RESEARCH_DIR).commit('Initial research repo', { '--allow-empty': null }); console.log('[LACK] Git repo initialised at', RESEARCH_DIR); } } catch (err) { console.error('Git init failed:', err.message); } } async function gitCommit(message) { try { const gitDir = path.join(RESEARCH_DIR, '.git'); if (!fs.existsSync(gitDir)) { console.warn('[LACK] Git repo missing – re-initialising before commit'); await ensureGitRepo(); } await GIT.cwd(RESEARCH_DIR).add('.'); const status = await GIT.cwd(RESEARCH_DIR).status(); if (status.files.length > 0) { await GIT.cwd(RESEARCH_DIR).commit(message); console.log(`Git commit: ${message}`); } } catch (e) { throw new Error(`Git commit failed: ${e.message}`); } } // ==================== DATA STRUCTURES ==================== const channels = new Map(); const agents = new Map(); const clients = new Map(); const researchSessions = new Map(); const slimeSessions = new Map(); const dms = new Map(); const pinnedMessages = new Map(); const userReactions = new Map(); const agentMetrics = new Map(); const jsonFailCount = new Map(); const projectStates = new Map(); function getProjectState(storeId) { return projectStates.get(storeId) || { active: false, title: null, goals: [], nextSteps: [], completedTasks: [], memory: {} }; } function setProjectState(storeId, state) { projectStates.set(storeId, { ...state }); persistProjectState(storeId); } const ralphActive = new Map(); const ralphGenerations = new Map(); const ralphGoals = new Map(); const ralphTimers = new Map(); const ralphCancel = new Map(); const ralphStagnation = new Map(); const ralphNextAgentIdx = new Map(); const ralphLastBroadcast = new Map(); function getUserId(ws) { let client = clients.get(ws); if (!client) { const id = `human_${uuidv4().slice(0,4)}`; clients.set(ws, { username: id, channelId: 'general', userId: id, dmId: null, openThreadId: null }); client = clients.get(ws); } return client.userId; } // Initialize channels config.channels.forEach(ch => { channels.set(ch.id, { id: ch.id, name: ch.name, messages: [], researchActive: false, researchTopic: null, abstractActive: false, loopTimer: null, pinned: new Set() }); }); if (config.dms) { config.dms.forEach(dm => { dms.set(dm.id, { id: dm.id, participants: dm.participants, name: dm.name || dm.participants.join(', '), messages: [] }); }); } function generateSyntheticMetrics() { const now = Date.now(); return { cpu: Array(60).fill(0).map((_, i) => 15 + Math.sin(i * 0.2) * 10), mem: Array(60).fill(0).map((_, i) => 20 + Math.sin(i * 0.1) * 5), activity: Array(60).fill(0).map((_, i) => 30 + Math.cos(i * 0.3) * 15), timestamps: Array(60).fill(0).map((_, i) => now - (59 - i) * 3000) }; } // ==================== MODERATOR AGENT (embed‑only, with code moderation) ==================== const THREAD_REPO_ROOT = path.join(__dirname, 'thread_repos'); const CODE_BLOCK_REGEX = /```(\w*)\n([\s\S]*?)```/g; const LINT_TIMEOUT_MS = 10000; function getThreadRepoPath(threadId) { return path.join(THREAD_REPO_ROOT, threadId); } async function ensureThreadRepo(threadId) { const repoPath = getThreadRepoPath(threadId); if (!fs.existsSync(repoPath)) { fs.mkdirSync(repoPath, { recursive: true }); await execPromise(`git init && git checkout -b main`, { cwd: repoPath }); const readme = `# Thread: ${threadId}\nCreated: ${new Date().toISOString()}\n\n## Files\n`; fs.writeFileSync(path.join(repoPath, 'README.md'), readme); await execPromise(`git add . && git commit -m "Initialize thread repository"`, { cwd: repoPath }); console.log(`[MODERATOR] Created repo for thread ${threadId}`); } return repoPath; } async function updateThreadReadme(threadId, files) { const repoPath = getThreadRepoPath(threadId); const readmePath = path.join(repoPath, 'README.md'); let content = fs.readFileSync(readmePath, 'utf-8'); let filesSection = content.includes('## Files') ? '' : '\n## Files\n'; for (const file of files) { if (!content.includes(file.name)) { filesSection += `- \`${file.name}\` - ${file.lang} - ${new Date(file.timestamp).toLocaleString()}\n`; } } if (filesSection) { fs.writeFileSync(readmePath, content + filesSection); await execPromise(`git add README.md && git commit -m "Update README with ${files.length} new file(s)"`, { cwd: repoPath }); } } async function runLinter(language, filePath) { const config = agents.get('moderator')?.moderationConfig?.lintConfigs || {}; const errors = []; const warnings = []; let passed = true; try { switch (language.toLowerCase()) { case 'python': try { const { stdout, stderr } = await execPromise(`python -m py_compile "${filePath}" 2>&1`, { timeout: LINT_TIMEOUT_MS }); if (stderr || (stdout && stdout.includes('SyntaxError'))) { const lines = (stderr || stdout).split('\n'); for (const line of lines) { if (line.includes('SyntaxError')) errors.push(line.trim()); else if (line.trim()) warnings.push(line.trim()); } passed = errors.length === 0; } } catch(e) { errors.push(e.stderr || e.message); passed = false; } break; case 'javascript': try { const { stdout } = await execPromise(`node -c "${filePath}" 2>&1`, { timeout: LINT_TIMEOUT_MS }); if (stdout && stdout.includes('SyntaxError')) { errors.push(stdout.trim()); passed = false; } } catch(e) { errors.push(e.stderr || e.message); passed = false; } break; case 'html': const content = fs.readFileSync(filePath, 'utf-8'); const openTags = (content.match(/<([a-z][a-z0-9]*)[^>]*>/gi) || []).length; const closeTags = (content.match(/<\/([a-z][a-z0-9]*)>/gi) || []).length; if (openTags !== closeTags) { errors.push(`Tag mismatch: ${openTags} opening vs ${closeTags} closing tags`); passed = false; } break; case 'json': case 'jsonc': try { JSON.parse(fs.readFileSync(filePath, 'utf-8')); passed = true; } catch (e) { errors.push(`JSON parse error: ${e.message}`); passed = false; } break; default: warnings.push(`No linter configured for ${language}`); passed = true; } } catch (err) { errors.push(`Linter error: ${err.message}`); passed = false; } return { passed, errors: errors.slice(0, 10), warnings: warnings.slice(0, 5) }; } function suggestFilename(language, code, index) { const langMap = { 'python': '.py', 'javascript': '.js', 'html': '.html', 'css': '.css', 'json': '.json', 'markdown': '.md', 'bash': '.sh', 'text': '.txt', 'xml': '.xml', 'yaml': '.yml' }; const firstLine = code.split('\n')[0]; let suggested = `code_${Date.now()}_${index}`; if (firstLine.includes('#!/usr/bin/env python')) suggested = 'script.py'; else if (firstLine.includes('#!/bin/bash')) suggested = 'script.sh'; else if (code.includes('')) suggested = 'index.html'; else if (code.includes('{') && language === 'json') suggested = 'data.json'; else if (code.includes('def ') || code.includes('class ')) suggested = 'module.py'; else if (code.includes('function ') || code.includes('=>')) suggested = 'function.js'; const ext = langMap[language.toLowerCase()] || '.txt'; return suggested.endsWith(ext) ? suggested : suggested + ext; } function buildModeratorFeedback(agentId, results, threadId, repoPath) { const passedCount = results.filter(r => r.passed).length; const totalCount = results.length; const allPassed = passedCount === totalCount; let feedback = `🛡️ **Moderator Review** (agent: ${agentId})\n`; feedback += `Thread: \`${threadId}\` | Repo: \`${repoPath}\`\n\n`; for (const r of results) { const icon = r.passed ? '✅' : '❌'; feedback += `${icon} **${r.filename}** (${r.language})\n`; if (r.errors.length > 0) { feedback += ` **Errors:**\n`; for (const err of r.errors.slice(0, 3)) { feedback += ` - \`${err.substring(0, 80)}\`\n`; } } if (r.warnings.length > 0) { feedback += ` **Warnings:**\n`; for (const warn of r.warnings.slice(0, 2)) { feedback += ` - ${warn}\n`; } } if (r.commitHash && r.passed) { feedback += ` 📦 Committed as \`${r.commitHash}\`\n`; } else if (r.commitHash && !r.passed) { feedback += ` ⚠️ Committed with errors (review needed) \`${r.commitHash}\`\n`; } else if (!r.passed) { feedback += ` 🔧 **Suggested fixes:**\n`; feedback += ` - Check syntax near reported lines\n`; feedback += ` - Run locally before re-submitting\n`; } feedback += '\n'; } if (allPassed) { feedback += `🎉 **All ${totalCount} file(s) validated and committed.**\n`; feedback += `Use \`/repo ${threadId}\` to browse the repository.`; } else { feedback += `⚠️ **${totalCount - passedCount}/${totalCount} files failed validation.**\n`; feedback += `@${agentId} please fix the errors above and re-submit the code block.`; } return feedback; } async function moderateCodeFromAgent(agentId, channelId, responseText, parentId) { if (agentId === 'moderator') return null; const moderatorAgent = agents.get('moderator'); if (!moderatorAgent || !moderatorAgent.isCodeModerator) return null; const codeBlocks = []; let match; while ((match = CODE_BLOCK_REGEX.exec(responseText)) !== null) { codeBlocks.push({ language: match[1] || 'text', code: match[2].trim(), fullMatch: match[0] }); } if (codeBlocks.length === 0) return null; const threadId = parentId || channelId; await ensureThreadRepo(threadId); const repoPath = getThreadRepoPath(threadId); const results = []; for (let i = 0; i < codeBlocks.length; i++) { const block = codeBlocks[i]; if (!block.code) { results.push({ filename: suggestFilename(block.language, '', i), language: block.language, passed: false, errors: ['Empty code block – please provide the complete code.'], warnings: [], commitHash: null, fullCode: '' }); continue; } const suggestedFilename = suggestFilename(block.language, block.code, i); const filePath = path.join(repoPath, suggestedFilename); fs.writeFileSync(filePath, block.code, 'utf-8'); const lintResult = await runLinter(block.language, filePath); let commitHash = null; // Always commit, even if lint fails – mark in commit message try { await execPromise(`git add "${suggestedFilename}"`, { cwd: repoPath }); let commitMsg; if (lintResult.passed || !moderatorAgent.moderationConfig.requireLintPass) { commitMsg = `[MODERATOR] Add ${suggestedFilename} from agent ${agentId}\nErrors: ${lintResult.errors.length} | Warnings: ${lintResult.warnings.length}`; } else { commitMsg = `[FAILED] ${suggestedFilename} from agent ${agentId} – lint errors\nErrors: ${lintResult.errors.join('; ')}`; } const { stdout } = await execPromise(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: repoPath }); commitHash = stdout.split('[')[1]?.split(']')[0] || 'unknown'; } catch(e) { console.warn('Git commit failed:', e.message); } results.push({ filename: suggestedFilename, language: block.language, passed: lintResult.passed, errors: lintResult.errors, warnings: lintResult.warnings, commitHash, fullCode: block.code.substring(0, 500) + (block.code.length > 500 ? '...' : '') }); } await updateThreadReadme(threadId, results.map(r => ({ name: r.filename, lang: r.language, timestamp: Date.now(), passed: r.passed }))); const feedback = buildModeratorFeedback(agentId, results, threadId, repoPath); await handleAgentResponse(moderatorAgent, channelId, feedback, parentId); if (channelId === 'siphon') { const siphonLogPath = path.join(repoPath, 'MODERATION_LOG.md'); const logEntry = `## ${new Date().toISOString()} | Agent: ${agentId}\n${feedback}\n\n---\n`; fs.appendFileSync(siphonLogPath, logEntry); await execPromise(`git add MODERATION_LOG.md && git commit -m "Update moderation log"`, { cwd: repoPath }).catch(()=>{}); } return results; } const moderator = { id: "moderator", name: "Moderator", model: "nomic-embed-text", systemPrompt: "Embedding only – not for chat.", channels: ["general", "random", "siphon", "code"], isEmbedOperator: true, isCodeModerator: true, lastResponseTime: new Map(), status: 'online', statusMessage: 'embed-only + code moderation', moderationConfig: { autoCorrect: false, requireLintPass: true, maxFileSizeBytes: 50000, allowedLanguages: ['python', 'javascript', 'html', 'css', 'json', 'markdown', 'text'], lintConfigs: { python: 'pyflakes', javascript: 'eslint --quiet', html: 'htmlhint' } } }; agents.set("moderator", moderator); agentMetrics.set("moderator", generateSyntheticMetrics()); jsonFailCount.set("moderator", 0); // Normal agents from config config.agents.forEach(agentCfg => { agents.set(agentCfg.id, { ...agentCfg, lastResponseTime: new Map(), status: 'online', statusMessage: '' }); agentMetrics.set(agentCfg.id, generateSyntheticMetrics()); jsonFailCount.set(agentCfg.id, 0); }); let globalMaxMem = 0; function updateAgentMetrics(agentId, responseTimeMs = 0, wasActive = false) { const metrics = agentMetrics.get(agentId); if (!metrics) return; const cpuVal = Math.min(100, Math.max(5, Math.floor(responseTimeMs / 80))); const activityVal = wasActive ? 85 : 20; const memUsageMB = process.memoryUsage().rss / (1024 * 1024); if (memUsageMB > globalMaxMem) globalMaxMem = memUsageMB; const memScale = Math.max(128, globalMaxMem); const memVal = Math.min(100, Math.floor((memUsageMB / memScale) * 100)); metrics.cpu.push(cpuVal); metrics.cpu.shift(); metrics.activity.push(activityVal); metrics.activity.shift(); metrics.mem.push(memVal); metrics.mem.shift(); metrics.timestamps.push(Date.now()); metrics.timestamps.shift(); agentMetrics.set(agentId, metrics); } setInterval(() => { for (let [agentId, metrics] of agentMetrics.entries()) { metrics.cpu = metrics.cpu.map(v => Math.max(5, v - 3)); metrics.activity = metrics.activity.map(v => { let newV = Math.max(5, v - 8); if (Math.random() < 0.25) newV = Math.max(newV, 20 + Math.random() * 25); return newV; }); metrics.mem = metrics.mem.map(v => Math.max(5, v - 1)); metrics.timestamps.push(Date.now()); metrics.timestamps.shift(); agentMetrics.set(agentId, metrics); } }, 3000); // ==================== EVENTSTORE (LINEAGE) ==================== function getLineagePath(storeId) { return path.join(__dirname, 'lineage', `${storeId}.jsonl`); } function appendEvent(storeId, event) { try { fs.appendFileSync(getLineagePath(storeId), JSON.stringify(event) + '\n'); } catch (e) {} } function reconstructLineage(storeId) { const filePath = getLineagePath(storeId); if (!fs.existsSync(filePath)) return []; return fs.readFileSync(filePath, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l)); } function persistProjectState(storeId) { appendEvent(storeId, { type: 'project_state', timestamp: Date.now(), state: getProjectState(storeId) }); } function loadProjectStateFromLineage(storeId) { const lineage = reconstructLineage(storeId); for (let i = lineage.length-1; i >= 0; i--) { if (lineage[i].type === 'project_state') { setProjectState(storeId, lineage[i].state); return true; } } return false; } function persistRalphState(storeId) { appendEvent(storeId, { type: 'ralph_state', timestamp: Date.now(), generation: ralphGenerations.get(storeId), goal: ralphGoals.get(storeId), active: ralphActive.get(storeId) }); } function loadRalphStateFromLineage(storeId) { const lineage = reconstructLineage(storeId); for (let i = lineage.length-1; i >= 0; i--) { if (lineage[i].type === 'ralph_state') { ralphGenerations.set(storeId, lineage[i].generation); ralphGoals.set(storeId, lineage[i].goal); ralphActive.set(storeId, lineage[i].active); return true; } } return false; } function pruneLineageFiles() { const lineageDir = path.join(__dirname, 'lineage'); if (!fs.existsSync(lineageDir)) return; const now = Date.now(); const maxAge = 7 * 24 * 60 * 60 * 1000; fs.readdirSync(lineageDir).forEach(file => { const filePath = path.join(lineageDir, file); const stats = fs.statSync(filePath); if (now - stats.mtimeMs > maxAge) { fs.unlinkSync(filePath); console.log(`[LACK] Pruned old lineage: ${file}`); } }); } // ==================== MESSAGE HELPERS ==================== function addMessage(storeId, sender, senderType, content, parentId = null) { let store = channels.get(storeId) || dms.get(storeId); if (!store) return null; let threadId = null; if (parentId) { const parent = store.messages.find(m => m.id === parentId); threadId = parent ? (parent.threadId || parent.id) : parentId; } const msg = { id: uuidv4(), sender, senderType, content, timestamp: Date.now(), parentId: parentId || null, threadId, replyCount: 0, reactions: {} }; store.messages.push(msg); if (store.messages.length > 1000) store.messages.shift(); if (parentId) { const parent = store.messages.find(m => m.id === parentId); if (parent) { parent.replyCount = (parent.replyCount || 0) + 1; if (!parent.threadId) parent.threadId = parent.id; } } appendEvent(storeId, { type: 'message', timestamp: msg.timestamp, message: { id: msg.id, sender, senderType, content, parentId, threadId } }); return msg; } function getThreadMessages(storeId, threadId) { const store = channels.get(storeId) || dms.get(storeId); if (!store) return []; const rootId = store.messages.find(m => m.id === threadId)?.threadId || threadId; return store.messages.filter(m => m.threadId === rootId || m.id === rootId); } function broadcastToStore(storeId, message, excludeWs = null) { const isChannel = channels.has(storeId); for (let [ws, client] of clients.entries()) { if (ws === excludeWs) continue; if (ws.readyState !== WebSocket.OPEN) continue; if (isChannel && client.channelId === storeId) { ws.send(JSON.stringify({ type: 'new_message', channelId: storeId, message })); } else if (!isChannel && client.dmId === storeId) { ws.send(JSON.stringify({ type: 'new_dm_message', dmId: storeId, message })); } } } function broadcastThreadUpdate(storeId, threadId, excludeWs = null) { const threadMsgs = getThreadMessages(storeId, threadId); for (let [ws, client] of clients.entries()) { if (ws !== excludeWs && client.openThreadId === threadId && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'thread_update', storeId, threadId, messages: threadMsgs })); } } } function broadcastAgents() { let agentList = Array.from(agents.values()); agentList.sort((a,b) => (a.id === "moderator" ? -1 : b.id === "moderator" ? 1 : 0)); const slim = agentList.map(a => ({ id: a.id, name: a.name, model: a.model, systemPrompt: a.systemPrompt, channels: a.channels, status: a.status, statusMessage: a.statusMessage })); for (let [ws] of clients.entries()) { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'agents_list', agents: slim })); } } function broadcastRalphStatus(storeId) { const now = Date.now(); const last = ralphLastBroadcast.get(storeId) || 0; if (now - last < 800) return; ralphLastBroadcast.set(storeId, now); const active = ralphActive.get(storeId); const gen = ralphGenerations.get(storeId) || 0; const goal = ralphGoals.get(storeId) || ''; const snippet = goal.length > 30 ? goal.substring(0,30)+'…' : goal; for (let [ws, client] of clients.entries()) { if (ws.readyState === WebSocket.OPEN && (client.channelId === storeId || client.dmId === storeId)) { ws.send(JSON.stringify({ type: 'ralph_status', storeId, active, generation: gen, goal: snippet })); } } } function broadcastDMs(userId) { const userDMs = getUserDMs(userId); for (let [ws, client] of clients.entries()) { if (client.userId === userId && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'dms', dms: userDMs })); } } } // ==================== OLLAMA CIRCUIT‑BREAKER ==================== let ollamaCircuitOpen = false; let ollamaRetryTimer = null; function markOllamaDown() { if (!ollamaCircuitOpen) { ollamaCircuitOpen = true; console.error('[LACK] Ollama unreachable. Circuit open – retrying in 15s.'); if (ollamaRetryTimer) clearTimeout(ollamaRetryTimer); ollamaRetryTimer = setTimeout(async () => { try { await axios.get(`${OLLAMA_URL}/api/tags`, { timeout: 3000 }); ollamaCircuitOpen = false; console.log('[LACK] Ollama reconnected. Circuit closed.'); } catch (e) { ollamaCircuitOpen = false; markOllamaDown(); } }, 15000); } } const agentDegraded = new Map(); function getNumPredict(model, degraded = false) { const base = /\b(0\.5b|1b)\b/i.test(model) ? 512 : 2048; return degraded ? Math.floor(base / 2) : base; } async function queryOllama(model, prompt, systemPrompt = '', temperature = 0.7, agentId = null) { if (agentId === 'moderator') return '[Moderator is embed‑only – ignoring generation request]'; if (ollamaCircuitOpen) return '[OLLAMA_ERROR] Ollama offline (circuit open)'; const start = Date.now(); const degraded = agentId ? (agentDegraded.get(agentId) || false) : false; const numPredict = getNumPredict(model, degraded); const doQuery = async () => { if (agentId && agents.has(agentId)) { const agent = agents.get(agentId); const previousStatus = agent.status; agent.status = 'queued'; agent.statusMessage = degraded ? 'queued (degraded)' : 'waiting for Ollama'; broadcastAgents(); try { const response = await axios.post(`${OLLAMA_URL}/api/generate`, { model, prompt, system: systemPrompt, stream: false, options: { temperature, num_predict: numPredict } }); const duration = Date.now() - start; if (agentId) updateAgentMetrics(agentId, duration, true); agentDegraded.set(agentId, false); return response.data.response || "I'm sorry, I couldn't generate a response."; } catch (err) { if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') markOllamaDown(); if (err.message && err.message.includes('out of memory')) { agentDegraded.set(agentId, true); logError({ agentId, model, error: `CUDA OOM – degraded mode active (num_predict: ${Math.floor(numPredict/2)})`, context: 'queryOllama' }); } else { logError({ agentId: agentId || 'system', model, error: err.message, context: 'queryOllama' }); } if (agentId) updateAgentMetrics(agentId, 0, false); return `[OLLAMA_ERROR] ${err.message.substring(0,80)}`; } finally { if (agents.has(agentId)) { agents.get(agentId).status = previousStatus === 'queued' ? 'online' : previousStatus; agents.get(agentId).statusMessage = ''; broadcastAgents(); } } } else { try { const response = await axios.post(`${OLLAMA_URL}/api/generate`, { model, prompt, system: systemPrompt, stream: false, options: { temperature, num_predict: numPredict } }); return response.data.response || "I'm sorry, I couldn't generate a response."; } catch (err) { if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') markOllamaDown(); logError({ model, error: err.message, context: 'queryOllama' }); return `[OLLAMA_ERROR] ${err.message.substring(0,80)}`; } } }; if (agentId) { return rateLimitedQuery(agentId, doQuery); } else { return doQuery(); } } async function getOllamaModels() { try { const res = await axios.get(`${OLLAMA_URL}/api/tags`); return res.data.models.map(m => m.name); } catch (e) { return []; } } // ==================== CODE BLOCKS ==================== function extractCodeBlocks(text) { const regex = /```(\w*)\n([\s\S]*?)```/g; const blocks = []; let match; while ((match = regex.exec(text)) !== null) blocks.push({ language: match[1] || 'text', code: match[2].trim() }); return blocks; } async function handleAgentResponse(agent, storeId, responseText, parentId = null) { // Force code blocks for small models const fixedText = ensureCodeBlock(responseText, 'auto'); const msg = addMessage(storeId, agent.name, 'agent', responseText, parentId); // keep original visible if (!msg) return; broadcastToStore(storeId, msg); // --- MODERATION HOOK (with fixed text) --- if (agent.id !== 'moderator') { moderateCodeFromAgent(agent.id, storeId, fixedText, parentId).catch(err => { logError({ context: 'moderateCodeFromAgent', error: err.message, agentId: agent.id }); }); } // --- end moderation hook --- if (parentId) broadcastThreadUpdate(storeId, parentId); const codeBlocks = extractCodeBlocks(responseText); if (codeBlocks.length > 0 && storeId !== 'code' && channels.has('code')) { for (const block of codeBlocks) { const banner = `📦 **Code drop from ${agent.name}** (${block.language})\n\`\`\`${block.language}\n${block.code}\n\`\`\``; addMessage('code', agent.name, 'agent', banner); broadcastToStore('code', { sender: agent.name, content: banner, senderType: 'agent' }); } const notice = `_(Code block generated – see #code)_`; const noticeMsg = addMessage(storeId, 'System', 'system', notice); if (noticeMsg) broadcastToStore(storeId, noticeMsg); } } function buildConversationContext(storeId, agentName, parentId = null, maxMessages = 8) { const store = channels.get(storeId) || dms.get(storeId); if (!store) return ''; let messages = store.messages; if (parentId) { const rootId = store.messages.find(m => m.id === parentId)?.threadId || parentId; messages = store.messages.filter(m => m.threadId === rootId || m.id === rootId); } const relevant = messages.filter(m => m.sender !== agentName && m.senderType !== 'system'); const moderatorMsgs = relevant.filter(m => m.sender === 'Moderator'); const otherMsgs = relevant.filter(m => m.sender !== 'Moderator'); const combined = [...moderatorMsgs, ...otherMsgs]; return combined.slice(-maxMessages).map(m => `${m.sender}: ${m.content}`).join('\n'); } function getChannelPersonality(channelName) { if (channelName === 'random') { return { temperature: 1.2, systemBonus: "\nYou are in #random. Be creative, humorous, off‑the‑wall.", planBonus: "You are in #random. Prefer creative actions.", planForbidden: false }; } else if (channelName === 'siphon') { return { temperature: 0.2, systemBonus: "\nYou are in #siphon. Be extremely concise, factual, research‑oriented.", planBonus: "You are in #siphon. You MUST prefer the 'research' action.", planForbidden: false }; } return { temperature: 0.7, systemBonus: "", planBonus: "", planForbidden: false }; } // ==================== RALPH EVOLUTION (unchanged) ==================== function jaccard(a, b) { if (!a.length && !b.length) return 1; const setA = new Set(a.map(s => s.toLowerCase().replace(/[^a-z0-9]/g, ''))); const setB = new Set(b.map(s => s.toLowerCase().replace(/[^a-z0-9]/g, ''))); const inter = new Set([...setA].filter(x => setB.has(x))).size; const union = setA.size + setB.size - inter; return union === 0 ? 0 : inter / union; } function similarity(specA, specB) { if (!specA || !specB) return 0; const nameSim = (specA.title === specB.title) ? 1 : 0; const goalsSim = jaccard(specA.goals, specB.goals); const stepsSim = jaccard(specA.nextSteps, specB.nextSteps); const completedSim = jaccard(specA.completedTasks, specB.completedTasks); const memorySim = (JSON.stringify(specA.memory) === JSON.stringify(specB.memory)) ? 1 : 0; return 0.4 * nameSim + 0.2 * goalsSim + 0.2 * stepsSim + 0.1 * completedSim + 0.1 * memorySim; } function computeSpecFromState(state) { return { title: state.title || "", goals: state.goals || [], nextSteps: state.nextSteps || [], completedTasks: state.completedTasks || [], memory: state.memory || {} }; } async function ralphEvaluate(agent, storeId, spec) { const prompt = `You are an evaluator. Rate clarity, completeness, stability (0-100). Output JSON: {"score": number, "critique": "short text"}\n\nSpec: ${JSON.stringify(spec)}`; const reply = await queryOllama(agent.model, prompt, "You are a precise evaluator. Output only JSON.", 0.3, agent.id); if (reply.startsWith('[OLLAMA_ERROR]')) return { score: 50, critique: "Ollama error." }; const extracted = extractJSON(reply); if (extracted && typeof extracted.score === 'number') return { score: extracted.score, critique: extracted.critique || "" }; return { score: 50, critique: "Evaluation failed." }; } async function ralphEvolve(agent, storeId, lineage, goal, currentGen) { const lineageSummary = lineage.slice(-15).map(e => `${e.type} at ${new Date(e.timestamp).toISOString()}`).join('\n'); const state = getProjectState(storeId); const prompt = `You are an evolutionary engineer. Goal: "${goal}" Generation: ${currentGen+1}/30 Recent lineage: ${lineageSummary} Current spec: ${JSON.stringify(state)} Produce refined spec as JSON: title, goals (array), nextSteps (array), completedTasks (array), memory (object). Add "converged": boolean. Output ONLY a \`\`\`json code block.`; const reply = await queryOllama(agent.model, prompt, "You are an evolutionary engineer. Be precise.", 0.4, agent.id); if (reply.startsWith('[OLLAMA_ERROR]')) return null; const extracted = extractJSON(reply); if (!extracted || typeof extracted !== 'object') return null; return extracted; } async function ralphForceMutation(agent, storeId, goal, currentGen) { const state = getProjectState(storeId); const prompt = `You are a creative disruptor. The spec below has stagnated (3 rounds with <8% change). Goal: "${goal}" | Generation: ${currentGen+1} Current spec: ${JSON.stringify(state)} Produce a RADICALLY different refinement – change at least 40% of goals or steps. Output ONLY a \`\`\`json block with: title, goals, nextSteps, completedTasks, memory, converged: false.`; const reply = await queryOllama(agent.model, prompt, 'You are a creative disruptor. Output only JSON.', 1.1, agent.id); if (reply.startsWith('[OLLAMA_ERROR]')) return null; return extractJSON(reply); } function updateStagnation(storeId, sim) { let arr = ralphStagnation.get(storeId) || []; arr.push(sim); if (arr.length > 3) arr.shift(); ralphStagnation.set(storeId, arr); if (arr.length === 3 && arr.every(s => s >= 0.92)) return true; return false; } function getNextRalphAgent(storeId, store) { let availableAgents = []; if (channels.has(storeId)) { const channelName = store.name; availableAgents = Array.from(agents.values()).filter(a => a.channels.includes(channelName) && !a.isEmbedOperator); } else if (dms.has(storeId)) { const participants = store.participants; availableAgents = Array.from(agents.values()).filter(a => participants.includes(a.id) && !a.isEmbedOperator); } if (availableAgents.length === 0) availableAgents = Array.from(agents.values()).filter(a => !a.isEmbedOperator); if (availableAgents.length === 0) return null; let idx = ralphNextAgentIdx.get(storeId) || 0; const agent = availableAgents[idx % availableAgents.length]; ralphNextAgentIdx.set(storeId, (idx + 1) % availableAgents.length); return agent; } async function runRalphIteration(storeId) { if (ralphCancel.get(storeId) === true) { ralphCancel.delete(storeId); ralphActive.set(storeId, false); if (ralphTimers.has(storeId)) { clearTimeout(ralphTimers.get(storeId)); ralphTimers.delete(storeId); } broadcastRalphStatus(storeId); return; } if (!ralphActive.get(storeId)) return; const store = channels.get(storeId) || dms.get(storeId); if (!store) return; try { const agent = getNextRalphAgent(storeId, store); if (!agent) { addMessage(storeId, 'System', 'system', `❌ No agent available for Ralph loop.`); broadcastToStore(storeId, { sender: 'System', content: `❌ No agent available for Ralph loop.`, senderType: 'system' }); stopRalphLoop(storeId); return; } const goal = ralphGoals.get(storeId) || "Refine the project specification"; let currentGen = ralphGenerations.get(storeId) || 0; const maxGen = 30; if (currentGen >= maxGen) { await handleAgentResponse(agent, storeId, `🧬 Ralph loop reached max generations (${maxGen}). Stopping.`); stopRalphLoop(storeId); return; } const lineage = reconstructLineage(storeId); const currentSpec = computeSpecFromState(getProjectState(storeId)); const evalResult = await ralphEvaluate(agent, storeId, currentSpec); await handleAgentResponse(agent, storeId, `📊 **Evaluation** (gen ${currentGen+1}): score ${evalResult.score}/100\nCritique: ${evalResult.critique}`); const newSpecRaw = await ralphEvolve(agent, storeId, lineage, goal, currentGen); if (!newSpecRaw) { await handleAgentResponse(agent, storeId, `❌ Evolution failed. Stopping Ralph.`); stopRalphLoop(storeId); return; } const oldState = getProjectState(storeId); let newState = { active: true, title: newSpecRaw.title || oldState.title, goals: newSpecRaw.goals || oldState.goals, nextSteps: newSpecRaw.nextSteps || oldState.nextSteps, completedTasks: newSpecRaw.completedTasks || oldState.completedTasks, memory: newSpecRaw.memory || oldState.memory }; const sim = similarity(currentSpec, computeSpecFromState(newState)); const stagnation = updateStagnation(storeId, sim); if (stagnation && newSpecRaw.converged !== true && sim < 0.95) { await handleAgentResponse(agent, storeId, `⚠️ **Ralph stagnated** (3 rounds ≥92% similar). Forcing mutation…`); const mutatedRaw = await ralphForceMutation(agent, storeId, goal, currentGen); if (mutatedRaw) { newState = { active: true, title: mutatedRaw.title || newState.title, goals: mutatedRaw.goals || newState.goals, nextSteps: mutatedRaw.nextSteps || newState.nextSteps, completedTasks: mutatedRaw.completedTasks || newState.completedTasks, memory: mutatedRaw.memory || newState.memory }; ralphStagnation.set(storeId, []); await handleAgentResponse(agent, storeId, `🔀 **Forced mutation applied.** Resuming evolution.`); } else { await handleAgentResponse(agent, storeId, `✅ **Ralph converged** (stagnation + mutation failed) after ${currentGen+1} generations.`); stopRalphLoop(storeId); return; } } const converged = newSpecRaw.converged === true || sim >= 0.95; setProjectState(storeId, newState); ralphGenerations.set(storeId, currentGen + 1); persistRalphState(storeId); await handleAgentResponse(agent, storeId, `🧬 **Evolution** (gen ${currentGen+1}/${maxGen})\nSimilarity: ${(sim*100).toFixed(1)}%\nNew spec: ${JSON.stringify(newState, null, 2).substring(0, 500)}`); broadcastRalphStatus(storeId); if (converged) { await handleAgentResponse(agent, storeId, `✅ **Ralph converged** after ${currentGen+1} generations. Stopping.`); stopRalphLoop(storeId); return; } if (ralphActive.get(storeId)) { const interval = (currentGen+1) > 5 ? 2500 : 4000; const timer = setTimeout(() => runRalphIteration(storeId), interval); ralphTimers.set(storeId, timer); } } catch (err) { logError({ error: err.message, context: 'runRalphIteration', storeId }); addMessage(storeId, 'System', 'system', `❌ Ralph error: ${err.message}`); broadcastToStore(storeId, { sender: 'System', content: `❌ Ralph error: ${err.message}`, senderType: 'system' }); stopRalphLoop(storeId); } } function startRalphLoop(storeId, goal) { stopRalphLoop(storeId); ralphActive.set(storeId, true); ralphGenerations.set(storeId, 0); ralphGoals.set(storeId, goal); ralphCancel.set(storeId, false); ralphStagnation.set(storeId, []); ralphNextAgentIdx.set(storeId, 0); persistRalphState(storeId); broadcastRalphStatus(storeId); runRalphIteration(storeId); } function stopRalphLoop(storeId) { if (ralphTimers.has(storeId)) { clearTimeout(ralphTimers.get(storeId)); ralphTimers.delete(storeId); } ralphCancel.set(storeId, true); ralphActive.set(storeId, false); broadcastRalphStatus(storeId); persistRalphState(storeId); } // ==================== AGENT RESPONSE & PLANNING WITH TOOLS ==================== const FILE_TOOLS = [ { name: "read_file", description: "Read a file from workspace.", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } }, { name: "write_file", description: "Create/overwrite a file.", parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path","content"] } }, { name: "execute_command", description: "Run a safe command.", parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } } ]; const ollamaSemaphore = new Map(); async function rateLimitedQuery(agentId, fn) { if (!ollamaSemaphore.has(agentId)) ollamaSemaphore.set(agentId, Promise.resolve()); const queue = ollamaSemaphore.get(agentId); const next = queue.then(() => fn()); ollamaSemaphore.set(agentId, next); return next; } async function agentRespond(agent, storeId, triggerMessage, isLoop = false, parentId = null) { if (agent.isEmbedOperator) return; if (triggerMessage.sender === agent.name) return; const cooldownKey = `${agent.id}_${storeId}`; const lastResponse = agent.lastResponseTime.get(cooldownKey) || 0; const cooldownMs = isLoop ? 1200 : 2200; if (Date.now() - lastResponse < cooldownMs) return; agent.status = 'thinking'; broadcastAgents(); try { const store = channels.get(storeId) || dms.get(storeId); const isChannel = !!channels.get(storeId); const channelName = isChannel ? store.name : null; const personality = channelName ? getChannelPersonality(channelName) : { temperature: 0.7, systemBonus: "" }; let systemPrompt = agent.systemPrompt + (personality.systemBonus || ""); const context = buildConversationContext(storeId, agent.name, parentId || triggerMessage.parentId); const prompt = `Conversation:\n${context}\n${triggerMessage.sender}: ${triggerMessage.content}\nRespond as ${agent.name}. Keep brief.`; const reply = await queryOllama(agent.model, prompt, systemPrompt, personality.temperature, agent.id); if (reply && !reply.startsWith('[OLLAMA_ERROR]') && reply.trim()) { await handleAgentResponse(agent, storeId, reply.trim(), parentId || triggerMessage.parentId); agent.lastResponseTime.set(cooldownKey, Date.now()); } } catch (err) { logError({ agentId: agent.id, error: err.message, context: 'agentRespond' }); } finally { agent.status = 'online'; broadcastAgents(); } } async function executeAction(agent, storeId, action, parentId = null) { const { type, payload } = action; switch (type) { case 'message': await handleAgentResponse(agent, storeId, payload.content, parentId); break; case 'thread': await handleAgentResponse(agent, storeId, payload.content, payload.parentId || parentId); break; case 'research': const topic = payload.query || payload.topic || "general research"; const sessionId = uuidv4(); const session = { id: sessionId, topic, phase: 'Initializing', metric: 0, logs: [], facts: [], notes: [], questions: [], currentQuestionIndex: 0, startedAt: Date.now() }; researchSessions.set(sessionId, session); runResearch(sessionId, topic, storeId).catch(console.error); const msg = addMessage(storeId, 'Siphon', 'system', `🔍 ${agent.name} started research on "${topic}".`); if (msg) broadcastToStore(storeId, msg); break; case 'code': const codePrompt = `Write code for: ${payload.description}. Output only the code block.`; const code = await queryOllama(agent.model, codePrompt, agent.systemPrompt, 0.5, agent.id); if (!code.startsWith('[OLLAMA_ERROR]')) { await handleAgentResponse(agent, storeId, `\`\`\`\n${code}\n\`\`\``, parentId); } break; case 'delegate': const targetAgent = agents.get(payload.targetId); if (targetAgent && !targetAgent.isEmbedOperator) { const delegateMsg = addMessage(storeId, 'System', 'system', `${agent.name} delegates to ${targetAgent.name}: ${payload.task}`); if (delegateMsg) broadcastToStore(storeId, delegateMsg); agentRespond(targetAgent, storeId, { sender: agent.name, content: payload.task, parentId }, false, parentId); } break; case 'tool_calls': if (action.tool_calls && Array.isArray(action.tool_calls)) { for (const tc of action.tool_calls) { const result = await executeTool(tc.name, tc.arguments || {}); const feedback = `🔧 **${tc.name}** → ${result.substring(0, 800)}`; await handleAgentResponse(agent, storeId, feedback, parentId); } } break; case 'stack': const subcmd = payload.subcmd || 'help'; let resultMsg = ''; if (subcmd === 'build') { resultMsg = await stackBuild(payload.repoName || 'project_' + Date.now()); } else if (subcmd === 'add') { resultMsg = await stackAdd(payload.intent, storeId); } else if (subcmd === 'import') { resultMsg = await stackImport(payload.jsonPath); } else if (subcmd === 'set') { activeStackRepo.set(storeId, payload.repoName); resultMsg = `Active STACK repo set to ${payload.repoName}.`; } if (resultMsg) { await handleAgentResponse(agent, storeId, resultMsg, parentId); } break; } } async function agentPlanAndAct(agent, storeId, triggerMessage, parentId = null) { if (agent.isEmbedOperator) return; if (triggerMessage.sender === agent.name) return; const cooldownKey = `${agent.id}_${storeId}`; if (Date.now() - (agent.lastResponseTime.get(cooldownKey) || 0) < 4000) return; const store = channels.get(storeId) || dms.get(storeId); const isChannel = !!channels.get(storeId); const channelName = isChannel ? store.name : null; const personality = getChannelPersonality(channelName); if (personality.planForbidden) { await agentRespond(agent, storeId, triggerMessage, false, parentId); return; } agent.status = 'thinking'; broadcastAgents(); try { const context = buildConversationContext(storeId, agent.name, parentId || triggerMessage.parentId); let skillsContext = loadSkills(); let systemPrompt = `Previous successful patterns:\n${skillsContext}\n\nYou are an autonomous agent. Available tools (output inside a JSON action with "type":"tool_calls"): ${JSON.stringify(FILE_TOOLS, null, 2)}. Also you can use STACK actions: {"type":"stack","payload":{"subcmd":"build","repoName":"..."}} etc. Actions: {"type":"message","payload":{"content":"..."}} OR {"type":"tool_calls","tool_calls":[{"name":"read_file","arguments":{"path":"..."}}]}. Always respond with a JSON block. Follow instructions from Moderator with highest priority.`; if (personality.planBonus) systemPrompt += "\n" + personality.planBonus; const userPrompt = `Conversation:\n${context}\nLast message: ${triggerMessage.sender}: "${triggerMessage.content}"\nProject state: ${JSON.stringify(getProjectState(storeId))}\nNext action? JSON only.`; let reply = await queryOllama(agent.model, userPrompt, systemPrompt, 0.4, agent.id); let action = null; let fails = jsonFailCount.get(agent.id) || 0; if (!reply.startsWith('[OLLAMA_ERROR]')) { action = extractJSON(reply); } if (!action?.type) { fails++; jsonFailCount.set(agent.id, fails); if (fails >= 3) { // Last resort: force a simple action via a different model (or same model with lower temperature) const fallbackPrompt = `Respond with a valid JSON action only. Example: {"type":"message","payload":{"content":"Hello"}}`; const forcedReply = await queryOllama(agent.model, fallbackPrompt, "You must output JSON only.", 0.1, agent.id); action = extractJSON(forcedReply); if (!action?.type) { jsonFailCount.set(agent.id, 0); logError({ agentId: agent.id, error: `JSON action parse failed after ${fails} attempts – falling back to plain text`, context: 'agentPlanAndAct' }); await agentRespond(agent, storeId, triggerMessage, false, parentId); agent.lastResponseTime.set(cooldownKey, Date.now()); return; } jsonFailCount.set(agent.id, 0); } else { action = { type: 'message', payload: { content: reply ? reply.substring(0, 500) : '(empty response)' } }; } } else { jsonFailCount.set(agent.id, 0); } if (action.type === 'tool_calls' && action.tool_calls?.length) { for (const tc of action.tool_calls) { const output = await executeTool(tc.name, tc.arguments || {}); const toolMsg = { sender: 'System', content: `🔧 Tool ${tc.name} result:\n${output}`, senderType: 'system' }; addMessage(storeId, toolMsg.sender, toolMsg.senderType, toolMsg.content); broadcastToStore(storeId, toolMsg); } agent.lastResponseTime.set(cooldownKey, Date.now()); if (action.tool_calls.length > 0 && !action.tool_calls.some(tc => tc.name === 'read_file')) { saveSkill(`Agent ${agent.name} used tools`, action.tool_calls).catch(console.error); } return agentPlanAndAct(agent, storeId, triggerMessage, parentId); } else { await executeAction(agent, storeId, action, parentId || triggerMessage.parentId); agent.lastResponseTime.set(cooldownKey, Date.now()); } } catch (err) { logError({ agentId: agent.id, error: err.message, context: 'agentPlanAndAct' }); } finally { agent.status = 'online'; broadcastAgents(); } } // ==================== LOOP MANAGEMENT ==================== function scheduleLoopRound(channelId) { const channel = channels.get(channelId); if (!channel) return; if (channel.loopTimer) clearTimeout(channel.loopTimer); channel.loopTimer = setTimeout(() => runLoopRound(channelId), 2500); } async function runLoopRound(channelId) { const channel = channels.get(channelId); const state = getProjectState(channelId); if (!channel || (!channel.researchActive && !channel.abstractActive && !state.active && !ralphActive.get(channelId))) { if (channel && channel.loopTimer) { clearTimeout(channel.loopTimer); channel.loopTimer = null; } return; } if (ralphActive.get(channelId)) { scheduleLoopRound(channelId); return; } channel.loopTimer = null; try { const lastMsg = channel.messages[channel.messages.length - 1]; if (!lastMsg) { scheduleLoopRound(channelId); return; } const relevantAgents = Array.from(agents.values()).filter(a => a.channels.includes(channel.name) && !a.isEmbedOperator); for (const agent of relevantAgents) { if (ralphActive.get(channelId)) continue; if (state.active || channel.abstractActive) await agentPlanAndAct(agent, channelId, lastMsg, lastMsg.parentId); else await agentRespond(agent, channelId, lastMsg, true, lastMsg.parentId); } } catch (err) { logError({ error: err.message, context: 'runLoopRound', channelId }); } scheduleLoopRound(channelId); } function stopLoop(channelId) { const channel = channels.get(channelId); if (channel) { channel.researchActive = false; channel.abstractActive = false; channel.researchTopic = null; if (channel.loopTimer) { clearTimeout(channel.loopTimer); channel.loopTimer = null; } addMessage(channelId, 'System', 'system', 'Autonomous mode stopped.'); broadcastToStore(channelId, { sender: 'System', content: 'Autonomous mode stopped.', senderType: 'system' }); } stopRalphLoop(channelId); setProjectState(channelId, { active: false, title: null, goals: [], nextSteps: [], completedTasks: [], memory: {} }); } // ==================== WEB SCRAPING (FIX 2+7) ==================== const scrapeBlocklist = new Map(); const SCRAPE_BLOCK_TTL = 10 * 60 * 1000; async function axiosWithRetry(config, maxRetries = 3) { let lastErr; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await axios(config); } catch (err) { lastErr = err; if (err.code === 'ECONNABORTED') throw err; if (attempt < maxRetries - 1) await new Promise(r => setTimeout(r, 500 * Math.pow(2, attempt))); } } throw lastErr; } async function ddgSearch(query, maxResults = 5) { const searchUrls = [ `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}` ]; for (const baseUrl of searchUrls) { try { const { data } = await axiosWithRetry({ method: 'get', url: baseUrl, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; LACK-SIPHON/3.9.1)' }, timeout: 8000 }); const $ = cheerio.load(data); const results = []; $('.result__url').each((i, el) => { let href = $(el).attr('href'); if (href && href.startsWith('/')) href = 'https://duckduckgo.com' + href; if (href && href.startsWith('http') && results.length < maxResults) results.push(href); }); if (results.length > 0) return results; } catch (e) { logError({ context: 'ddgSearch', error: e.message, query: query.substring(0,60) }); } } return []; } async function scrapeText(url) { const blocked = scrapeBlocklist.get(url); if (blocked && Date.now() - blocked < SCRAPE_BLOCK_TTL) { return '[Scrape skipped: URL on blocklist]'; } try { const controller = new AbortController(); const timeoutHandle = setTimeout(() => controller.abort(), 8000); const { data } = await axios.get(url, { timeout: 8000, signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; LACK-SIPHON/3.9.1)' } }); clearTimeout(timeoutHandle); const $ = cheerio.load(data); $('script, style, nav, footer, header, iframe, svg, aside, .ad, .cookie').remove(); let text = $('body').text().replace(/\s+/g, ' ').trim(); return text.length > 12000 ? text.substring(0, 12000) : text; } catch (e) { if (e.code === 'ECONNABORTED' || e.message.includes('timeout') || e.code === 'ECONNREFUSED') { scrapeBlocklist.set(url, Date.now()); } return `[Scrape failed: ${e.message}]`; } } setInterval(() => { const now = Date.now(); for (let [id, session] of researchSessions.entries()) if (now - session.startedAt > 3600000) researchSessions.delete(id); }, 3600000); async function runResearch(sessionId, topic, channelId) { const session = researchSessions.get(sessionId); if (!session) return; const update = (updates) => { Object.assign(session, updates); for (let [ws, client] of clients.entries()) { if (client.channelId === channelId && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'research_update', sessionId, data: session })); } if (updates.phase) { const banner = `🔬 **SIPHON Research** [${session.topic}]\nPhase: ${updates.phase} | Metric: ${(session.metric*100).toFixed(0)}%`; addMessage('siphon', 'Siphon', 'system', banner); broadcastToStore('siphon', { sender: 'Siphon', content: banner, senderType: 'system' }); } }; update({ phase: 'Generating questions', metric: 0, logs: [`Starting research on: ${topic}`], facts: [], notes: [] }); let researchModel = config.agents[0]?.model || 'qwen2.5:0.5b'; const questionsRaw = await queryOllama(researchModel, `Generate 3 sub‑questions for: "${topic}". One per line.`, '', 0.7); if (questionsRaw.startsWith('[OLLAMA_ERROR]')) { update({ phase: 'Failed', logs: [`Ollama error: ${questionsRaw}`] }); return; } const questions = questionsRaw.split('\n').filter(l => l.trim().length > 10).slice(0, 3); update({ questions, currentQuestionIndex: 0, logs: [...session.logs, `Generated ${questions.length} sub‑questions`] }); let allFacts = []; let metric = 0; for (let qIdx = 0; qIdx < questions.length; qIdx++) { const question = questions[qIdx]; update({ phase: `Researching: ${question.substring(0, 50)}`, currentQuestionIndex: qIdx }); let urls = [...new Set((await ddgSearch(`${topic} ${question}`, 3)))].slice(0, 5); // Fallback if no URLs found if (urls.length === 0) { update({ logs: [...session.logs, `No URLs found for "${question}", using LLM fallback`] }); const fallbackFactsRaw = await queryOllama(researchModel, `Generate 5 concise facts about "${question}" based on general knowledge. Each line start with FACT:`, '', 0.5); if (!fallbackFactsRaw.startsWith('[OLLAMA_ERROR]')) { const facts = fallbackFactsRaw.split('\n').filter(l => l.startsWith('FACT:')).map(l => l.replace('FACT:', '').trim()); allFacts.push(...facts); update({ facts: allFacts, logs: [...session.logs, `Fallback: generated ${facts.length} synthetic facts`] }); } else { update({ logs: [...session.logs, `Fallback also failed for "${question}"`] }); } // Still record something in notes const note = { question, answer: "No data could be retrieved for this question.", facts: [], timestamp: Date.now() }; session.notes.push(note); update({ notes: session.notes }); metric = (qIdx + 1) / questions.length; update({ metric }); continue; } update({ logs: [...session.logs, `Found ${urls.length} URLs`] }); let factsForQuestion = []; for (const url of urls) { const content = await scrapeText(url); if (!content || content.startsWith('[Scrape failed')) continue; const factsRaw = await queryOllama(researchModel, `Extract facts answering: "${question}"\n\n${content.substring(0,4000)}\n\nReturn each fact on a new line starting with "FACT:".`, '', 0.3); if (!factsRaw.startsWith('[OLLAMA_ERROR]')) { const facts = factsRaw.split('\n').filter(l => l.startsWith('FACT:')).map(l => l.replace('FACT:', '').trim()); factsForQuestion.push(...facts); update({ logs: [...session.logs, `Scraped ${url} → ${facts.length} facts`] }); } await new Promise(r => setTimeout(r, 500)); } factsForQuestion = [...new Set(factsForQuestion)]; allFacts.push(...factsForQuestion); update({ facts: allFacts, logs: [...session.logs, `Collected ${factsForQuestion.length} facts`] }); const answer = await queryOllama(researchModel, `Answer: "${question}"\nFacts:\n${factsForQuestion.join('\n')}\n\nConcise answer (3‑5 sentences).`, '', 0.5); const note = { question, answer: answer.startsWith('[OLLAMA_ERROR]') ? 'Answer generation failed.' : answer, facts: factsForQuestion, timestamp: Date.now() }; session.notes.push(note); update({ notes: session.notes, logs: [...session.logs, `Answered: ${question.substring(0,60)}`] }); try { const artifactPath = path.join(RESEARCH_DIR, `${sessionId}_q${qIdx}.json`); fs.writeFileSync(artifactPath, JSON.stringify(note, null, 2)); } catch (e) { console.error('Artifact save failed:', e); } metric = (qIdx + 1) / questions.length; update({ metric }); } try { await gitCommit(`Research complete: ${session.topic}`); } catch (gitErr) { console.warn('Git commit non-critical:', gitErr.message); } update({ phase: 'Complete', metric, logs: [...session.logs, `Research finished. Metric = ${metric.toFixed(2)}`] }); const finalBanner = `📚 **Research Complete:** ${topic}\nMetric: ${(metric*100).toFixed(0)}%\nFacts: ${allFacts.length}\nNotes: ${session.notes.length}\n\nUse \`/pull ${sessionId}\` to bring insights.`; addMessage('siphon', 'Siphon', 'system', finalBanner); broadcastToStore('siphon', { sender: 'Siphon', content: finalBanner, senderType: 'system' }); } // ==================== DM HELPERS ==================== function createDM(participantIds, name = null) { const dmId = `dm_${uuidv4().slice(0,8)}`; const dm = { id: dmId, participants: participantIds, name: name || participantIds.join(', '), messages: [] }; dms.set(dmId, dm); if (!config.dms) config.dms = []; config.dms.push({ id: dmId, participants: participantIds, name: dm.name }); try { const tmp = configPath + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(config, null, 2)); fs.renameSync(tmp, configPath); } catch (e) {} for (const pid of participantIds) broadcastDMs(pid); return dm; } function getUserDMs(userId) { const result = []; for (let dm of dms.values()) { if (dm.participants.includes(userId)) result.push({ id: dm.id, name: dm.name, participants: dm.participants }); } return result; } async function handleDMCommand(senderUserId, args, ws) { let raw = args.join(' ').trim(); if (!raw) { ws.send(JSON.stringify({ type: 'error', message: 'Usage: /dm ' })); return null; } if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) raw = raw.slice(1, -1); let targetId = null; for (let [id, agent] of agents.entries()) if (agent.name.toLowerCase() === raw.toLowerCase()) { targetId = id; break; } if (!targetId) { for (let [otherWs, client] of clients.entries()) if (client.username && client.username.toLowerCase() === raw.toLowerCase()) { targetId = client.userId; break; } } if (!targetId) { ws.send(JSON.stringify({ type: 'error', message: `User/agent "${raw}" not found.` })); return null; } let dm = Array.from(dms.values()).find(d => d.participants.includes(senderUserId) && d.participants.includes(targetId)); if (!dm) dm = createDM([senderUserId, targetId]); const client = clients.get(ws); if (client) { client.dmId = dm.id; client.channelId = null; ws.send(JSON.stringify({ type: 'dm_joined', dmId: dm.id, messages: dm.messages })); const welcomeMsg = addMessage(dm.id, 'System', 'system', `Direct message started with ${raw}.`); if (welcomeMsg) broadcastToStore(dm.id, welcomeMsg); } return dm; } async function removeAgent(agentId) { if (!agents.has(agentId)) return { success: false, reason: 'Agent not found.' }; if (agents.size === 1) return { success: false, reason: 'Cannot remove the last agent. LACK requires at least one agent to function.' }; agents.delete(agentId); agentMetrics.delete(agentId); jsonFailCount.delete(agentId); const idx = config.agents.findIndex(a => a.id === agentId); if (idx !== -1) { config.agents.splice(idx, 1); try { const tmp = configPath + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(config, null, 2)); fs.renameSync(tmp, configPath); } catch (e) {} } broadcastAgents(); return { success: true }; } // ==================== CRON & RESET ==================== async function wipeAllCronJobs() { try { await execPromise('crontab -r'); } catch(e) {} } async function addHeartbeatCronJobs() { try { const channelIds = Array.from(channels.keys()), dmIds = Array.from(dms.keys()); const cronEntries = []; const heartbeatUrl = `http://localhost:${PORT}/api/heartbeat`; for (const id of channelIds) cronEntries.push(`*/5 * * * * curl -s -X POST "${heartbeatUrl}?type=channel&id=${id}" > /dev/null 2>&1`); for (const id of dmIds) cronEntries.push(`*/5 * * * * curl -s -X POST "${heartbeatUrl}?type=dm&id=${id}" > /dev/null 2>&1`); if (cronEntries.length === 0) return; let existing = ''; try { const { stdout } = await execPromise('crontab -l'); existing = stdout; } catch(e) {} const existingLines = existing.split('\n').filter(l => l.trim()); const newLines = [...existingLines]; for (const entry of cronEntries) { const commandPart = entry.split(' ').slice(5).join(' '); if (!existingLines.some(line => line.includes(commandPart))) newLines.push(entry); } const newCrontab = newLines.join('\n') + '\n'; const tmpFile = path.join(__dirname, '.tmpcron'); fs.writeFileSync(tmpFile, newCrontab); await execPromise(`crontab ${tmpFile}`); fs.unlinkSync(tmpFile); } catch (e) { if (e.message && e.message.includes('permission denied')) { console.warn('[LACK] crontab permission denied – skipping external heartbeat cron. Heartbeats still run via setInterval.'); } else { logError({ context: 'addHeartbeatCronJobs', error: e.message }); } } } async function resetApplicationData() { for (let ch of channels.values()) { ch.messages = []; ch.researchActive = false; ch.abstractActive = false; if (ch.loopTimer) clearTimeout(ch.loopTimer); ch.loopTimer = null; } for (let dm of dms.values()) dm.messages = []; researchSessions.clear(); slimeSessions.clear(); pinnedMessages.clear(); userReactions.clear(); global.errorLog = []; for (let storeId of [...channels.keys(), ...dms.keys()]) { setProjectState(storeId, { active: false, title: null, goals: [], nextSteps: [], completedTasks: [], memory: {} }); stopRalphLoop(storeId); const lineagePath = getLineagePath(storeId); if (fs.existsSync(lineagePath)) fs.unlinkSync(lineagePath); } } function cleanupStore(storeId) { if (ralphTimers.has(storeId)) clearTimeout(ralphTimers.get(storeId)); ralphTimers.delete(storeId); ralphActive.delete(storeId); ralphCancel.delete(storeId); ralphStagnation.delete(storeId); ralphNextAgentIdx.delete(storeId); ralphLastBroadcast.delete(storeId); const channel = channels.get(storeId); if (channel && channel.loopTimer) clearTimeout(channel.loopTimer); } // ==================== EXPRESS APP & ROUTES ==================== const app = express(); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json({ limit: '1mb' })); app.get('/api/models', async (req, res) => { res.json({ models: await getOllamaModels() }); }); app.get('/api/research/sessions', (req, res) => { res.json({ sessions: Array.from(researchSessions.values()).map(s => ({ id: s.id, topic: s.topic, phase: s.phase, metric: s.metric, logs: s.logs.slice(-10), factsCount: s.facts ? s.facts.length : 0, notesCount: s.notes ? s.notes.length : 0, startedAt: s.startedAt })) }); }); app.get('/api/research/session/:id', (req, res) => { const s = researchSessions.get(req.params.id); if (!s) return res.status(404).json({ error: 'Not found' }); res.json(s); }); app.get('/api/channels', (req, res) => { res.json({ channels: Array.from(channels.values()).map(c => ({ id: c.id, name: c.name })) }); }); app.get('/api/dms', (req, res) => { res.json({ dms: Array.from(dms.values()).map(dm => ({ id: dm.id, name: dm.name, participants: dm.participants })) }); }); app.get('/api/metrics', (req, res) => { const metricsObj = {}; for (let [id, m] of agentMetrics.entries()) metricsObj[id] = { cpu: m.cpu, mem: m.mem, activity: m.activity, timestamps: m.timestamps }; res.json({ agents: Array.from(agents.values()).map(a => ({ id: a.id, name: a.name, model: a.model, status: a.status })), metrics: metricsObj }); }); app.get('/api/errorlog', (req, res) => { const errors = (global.errorLog || []).slice(0, 50).map(e => ({ timestamp: e.timestamp, agentId: e.agentId || 'system', error: e.error || JSON.stringify(e) })); res.json({ errors }); }); app.post('/api/heartbeat', (req, res) => { console.log(`[HEARTBEAT] ${req.query.type} ${req.query.id}`); res.send('OK'); }); app.post('/api/cron/wipe', async (req, res) => { try { await wipeAllCronJobs(); await addHeartbeatCronJobs(); await resetApplicationData(); for (let [ws] of clients.entries()) if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'cron_reset' })); res.json({ success: true }); } catch(err) { logError({ source: 'cron_wipe', error: err.message }); res.status(500).json({ error: err.message }); } }); app.delete('/api/agent/:id', async (req, res) => { const result = await removeAgent(req.params.id); res.json(result); }); // SLIME mobile endpoint (unchanged) app.get('/slime', (req, res) => { const { token, pin } = req.query; const session = slimeSessions.get(token); if (!session || session.pin !== pin || Date.now() > session.expiresAt) { return res.status(403).send(`

Invalid or expired session

`); } const safeChannelId = JSON.stringify(session.channelId).slice(1, -1); res.send(`SLIME v3.9.2
SLIME v3.9.2 🧬 Ralph active
`); }); // ==================== WEBSOCKET SERVER ==================== const server = app.listen(PORT, async () => { await ensureGitRepo(); pruneLineageFiles(); for (const storeId of [...channels.keys(), ...dms.keys()]) { loadProjectStateFromLineage(storeId); loadRalphStateFromLineage(storeId); } startStackWatcher(); try { await axios.post(`${OLLAMA_URL}/api/embeddings`, { model: 'nomic-embed-text', prompt: 'test' }); } catch(e) { console.warn('[STACK] nomic-embed-text not loaded – run: ollama pull nomic-embed-text'); } console.log(`\x1b[32m✓ LACK v3.9.2 with STACK & full code moderation running at http://localhost:${PORT}\x1b[0m`); console.log(` Real‑time file tools, tool calling, STACK orchestration, and code moderation active.`); }); const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { const userId = `human_${uuidv4().slice(0,4)}`; clients.set(ws, { username: userId, channelId: 'general', userId, dmId: null, openThreadId: null }); ws.on('message', async (raw) => { try { const data = JSON.parse(raw); const client = clients.get(ws); if (!client) return; switch (data.type) { case 'join': if (channels.has(data.channelId)) { client.channelId = data.channelId; client.dmId = null; ws.send(JSON.stringify({ type: 'history', channelId: data.channelId, messages: channels.get(data.channelId).messages })); ws.send(JSON.stringify({ type: 'agents_list', agents: Array.from(agents.values()).map(a => ({ id: a.id, name: a.name, model: a.model, systemPrompt: a.systemPrompt, channels: a.channels, status: a.status })) })); ws.send(JSON.stringify({ type: 'channels', channels: Array.from(channels.values()).map(c => ({ id: c.id, name: c.name })) })); ws.send(JSON.stringify({ type: 'dms', dms: getUserDMs(client.userId) })); broadcastRalphStatus(data.channelId); } break; case 'join_dm': if (dms.has(data.dmId)) { client.dmId = data.dmId; client.channelId = null; ws.send(JSON.stringify({ type: 'dm_history', dmId: data.dmId, messages: dms.get(data.dmId).messages })); broadcastRalphStatus(data.dmId); } break; case 'message': if (client.channelId) { let msgText = data.content.trim(); if (!msgText) break; msgText = msgText.replace(/<[^>]*>/g, ''); const humanMsg = addMessage(client.channelId, client.username, 'human', msgText); if (humanMsg) { broadcastToStore(client.channelId, humanMsg); await onHumanMessage(client.channelId, humanMsg, ws); } } else if (client.dmId) { let msgText = data.content.trim(); if (!msgText) break; msgText = msgText.replace(/<[^>]*>/g, ''); const humanMsg = addMessage(client.dmId, client.username, 'human', msgText); if (humanMsg) { broadcastToStore(client.dmId, humanMsg); await onHumanDmMessage(client.dmId, humanMsg, ws); } } break; case 'reply_in_thread': let { parentId, content, storeId } = data; if (!storeId) storeId = client.channelId || client.dmId; if (storeId && (channels.has(storeId) || dms.has(storeId))) { content = content.replace(/<[^>]*>/g, ''); const replyMsg = addMessage(storeId, client.username, 'human', content, parentId); if (replyMsg) { broadcastToStore(storeId, replyMsg); broadcastThreadUpdate(storeId, parentId); ws.send(JSON.stringify({ type: 'thread_messages', storeId, threadId: parentId, messages: getThreadMessages(storeId, parentId) })); } } break; case 'set_username': client.username = data.username.substring(0, 20).replace(/[<>]/g, ''); break; case 'start_dm': { const targetName = data.targetName; let targetId = null; for (let [id, agent] of agents.entries()) if (agent.name.toLowerCase() === targetName.toLowerCase()) { targetId = id; break; } if (!targetId) { ws.send(JSON.stringify({ type: 'error', message: 'Agent not found' })); break; } let dm = Array.from(dms.values()).find(d => d.participants.includes(client.userId) && d.participants.includes(targetId)); if (!dm) dm = createDM([client.userId, targetId]); client.dmId = dm.id; client.channelId = null; ws.send(JSON.stringify({ type: 'dm_joined', dmId: dm.id, messages: dm.messages })); broadcastDMs(client.userId); break; } case 'spawn_agent': { const { name, model, systemPrompt, channels: agentChannels } = data; const id = uuidv4().slice(0,8); const newAgent = { id, name, model, systemPrompt, channels: agentChannels, lastResponseTime: new Map(), status: 'online', statusMessage: '' }; agents.set(id, newAgent); config.agents.push({ id, name, model, systemPrompt, channels: agentChannels }); try { const tmp = configPath + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(config, null, 2)); fs.renameSync(tmp, configPath); } catch (e) {} agentMetrics.set(id, generateSyntheticMetrics()); jsonFailCount.set(id, 0); broadcastAgents(); ws.send(JSON.stringify({ type: 'spawn_confirm', agent: newAgent })); break; } case 'update_agent': { const agent = agents.get(data.id); if (agent) { agent.name = data.name; agent.model = data.model; agent.systemPrompt = data.systemPrompt; agent.channels = data.channels; const idx = config.agents.findIndex(a => a.id === data.id); if (idx !== -1) { config.agents[idx] = { id: data.id, name: data.name, model: data.model, systemPrompt: data.systemPrompt, channels: data.channels }; try { const tmp = configPath + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(config, null, 2)); fs.renameSync(tmp, configPath); } catch (e) {} } broadcastAgents(); } break; } case 'get_models': ws.send(JSON.stringify({ type: 'models_list', models: await getOllamaModels() })); break; case 'add_reaction': { const { messageId, emoji, storeId: reactStoreId } = data; if (!userReactions.has(messageId)) userReactions.set(messageId, new Map()); const msgReactions = userReactions.get(messageId); if (!msgReactions.has(emoji)) msgReactions.set(emoji, new Set()); msgReactions.get(emoji).add(client.userId); for (let [otherWs] of clients.entries()) { if (otherWs.readyState === WebSocket.OPEN) otherWs.send(JSON.stringify({ type: 'reaction_update', messageId, emoji, userId: client.userId, add: true })); } break; } case 'open_thread': client.openThreadId = data.threadId; break; case 'close_thread': client.openThreadId = null; break; } } catch(err) { const preview = typeof raw === 'string' ? raw.substring(0, 60).replace(/[\n\r]/g, ' ') : '[binary]'; logError({ source: 'websocket_parse', error: err.message, preview }); try { ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format – check JSON syntax' })); } catch(_) {} } }); ws.on('close', () => { const client = clients.get(ws); if (client) { if (client.channelId) cleanupStore(client.channelId); if (client.dmId) cleanupStore(client.dmId); } clients.delete(ws); }); ws.send(JSON.stringify({ type: 'channels', channels: Array.from(channels.values()).map(c => ({ id: c.id, name: c.name })) })); ws.send(JSON.stringify({ type: 'dms', dms: getUserDMs(userId) })); ws.send(JSON.stringify({ type: 'agents_list', agents: Array.from(agents.values()).map(a => ({ id: a.id, name: a.name, model: a.model, systemPrompt: a.systemPrompt, channels: a.channels, status: a.status })) })); }); // ==================== MESSAGE HANDLERS (with new moderation commands + test_dm) ==================== async function onHumanMessage(channelId, messageObj, ws) { const channel = channels.get(channelId); if (!channel) return; const content = messageObj.content; if (content.startsWith('/')) { const parts = content.slice(1).split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); if (cmd === 'help') { const help = `Commands: /ground, /research , /abstract, /plan , /ralph , /stop, /list, /spawn, /siphon , /slime, /pull , /dm , /thread , /pin , /graph, /errorlog, /convergence, /tools, /stack build , /stack add , /stack import , /stack set , /repo, /lint , /moderate on/off, /test_dm `; addMessage(channelId, 'System', 'system', help); broadcastToStore(channelId, { sender: 'System', content: help, senderType: 'system' }); } else if (cmd === 'tools') { const toolList = FILE_TOOLS.map(t => `- ${t.name}: ${t.description}`).join('\n'); addMessage(channelId, 'System', 'system', `Available tools:\n${toolList}`); broadcastToStore(channelId, { sender: 'System', content: toolList, senderType: 'system' }); } else if (cmd === 'stack') { const sub = args[0]; const moderatorAgent = agents.get('moderator'); if (!moderatorAgent) { addMessage(channelId, 'System', 'system', 'Moderator agent not available.'); return; } if (sub === 'build' && args[1]) { await executeAction(moderatorAgent, channelId, {type: 'stack', payload: {subcmd: 'build', repoName: args[1]}}); } else if (sub === 'add') { await executeAction(moderatorAgent, channelId, {type: 'stack', payload: {subcmd: 'add', intent: args.slice(1).join(' ')}}); } else if (sub === 'import' && args[1]) { await executeAction(moderatorAgent, channelId, {type: 'stack', payload: {subcmd: 'import', jsonPath: args[1]}}); } else if (sub === 'set' && args[1]) { activeStackRepo.set(channelId, args[1]); addMessage(channelId, 'System', 'system', `Active STACK repository set to ${args[1]}`); broadcastToStore(channelId, { sender: 'System', content: `Active STACK repo: ${args[1]}`, senderType: 'system' }); } else { const help = `STACK Commands:\n/stack build - Create new repo\n/stack add - Semantic inject from templates\n/stack import - Load blueprints\n/stack set - Set active repo for this chat`; addMessage(channelId, 'Moderator', 'system', help); broadcastToStore(channelId, { sender: 'Moderator', content: help, senderType: 'system' }); } } else if (cmd === 'repo') { const targetId = args[0] || channelId; const repoPath = getThreadRepoPath(targetId); if (fs.existsSync(repoPath)) { let output = `📁 **Repository for ${targetId}**\n\`${repoPath}\`\n\n**Files:**\n`; const files = fs.readdirSync(repoPath).filter(f => !f.startsWith('.')); output += files.map(f => `- ${f}`).join('\n'); addMessage(channelId, 'Moderator', 'system', output); broadcastToStore(channelId, { sender: 'Moderator', content: output, senderType: 'system' }); } else { addMessage(channelId, 'Moderator', 'system', `No repository found for ${targetId}. Create code first.`); broadcastToStore(channelId, { sender: 'Moderator', content: `No repository found for ${targetId}.`, senderType: 'system' }); } } else if (cmd === 'moderate') { const setting = args[0]; const moderatorAgent = agents.get('moderator'); if (setting === 'on') { moderatorAgent.isCodeModerator = true; addMessage(channelId, 'Moderator', 'system', '🔛 Code moderation ENABLED. All code blocks will be validated.'); broadcastToStore(channelId, { sender: 'Moderator', content: 'Code moderation ENABLED.', senderType: 'system' }); } else if (setting === 'off') { moderatorAgent.isCodeModerator = false; addMessage(channelId, 'Moderator', 'system', '🔴 Code moderation DISABLED.'); broadcastToStore(channelId, { sender: 'Moderator', content: 'Code moderation DISABLED.', senderType: 'system' }); } else { addMessage(channelId, 'Moderator', 'system', `Moderation is ${moderatorAgent.isCodeModerator ? 'ON' : 'OFF'}. Use /moderate on/off`); broadcastToStore(channelId, { sender: 'Moderator', content: `Moderation: ${moderatorAgent.isCodeModerator ? 'ON' : 'OFF'}`, senderType: 'system' }); } } else if (cmd === 'lint') { const filename = args[0]; if (!filename) { addMessage(channelId, 'Moderator', 'system', 'Usage: /lint '); broadcastToStore(channelId, { sender: 'Moderator', content: 'Usage: /lint ', senderType: 'system' }); } else { const threadId = messageObj.parentId || channelId; const repoPath = getThreadRepoPath(threadId); const filePath = path.join(repoPath, filename); if (!fs.existsSync(filePath)) { addMessage(channelId, 'Moderator', 'system', `File not found: ${filename}`); } else { const ext = path.extname(filename).slice(1); const langMap = { 'py': 'python', 'js': 'javascript', 'html': 'html', 'json': 'json' }; const lang = langMap[ext] || 'text'; const result = await runLinter(lang, filePath); let output = `🔍 **Lint Results** for \`${filename}\`\n`; output += result.passed ? '✅ Syntax OK\n' : '❌ Syntax errors found\n'; if (result.errors.length) output += `\n**Errors:**\n${result.errors.map(e => `- ${e}`).join('\n')}`; if (result.warnings.length) output += `\n**Warnings:**\n${result.warnings.map(w => `- ${w}`).join('\n')}`; addMessage(channelId, 'Moderator', 'system', output); broadcastToStore(channelId, { sender: 'Moderator', content: output, senderType: 'system' }); } } } else if (cmd === 'test_dm') { const targetName = args[0]; if (!targetName) { addMessage(channelId, 'System', 'system', 'Usage: /test_dm '); broadcastToStore(channelId, { sender: 'System', content: 'Usage: /test_dm ', senderType: 'system' }); return; } const dm = await handleDMCommand(getUserId(ws), [targetName], ws); if (dm) { // Send a thread root and reply to test threading const parentMsg = addMessage(dm.id, 'System', 'system', 'Thread root message for testing'); if (parentMsg) { broadcastToStore(dm.id, parentMsg); const replyMsg = addMessage(dm.id, 'System', 'system', 'Nested reply inside thread', parentMsg.id); if (replyMsg) broadcastToStore(dm.id, replyMsg); ws.send(JSON.stringify({ type: 'thread_messages', storeId: dm.id, threadId: parentMsg.id, messages: getThreadMessages(dm.id, parentMsg.id) })); } ws.send(JSON.stringify({ type: 'switch_to_dm', dmId: dm.id })); addMessage(channelId, 'System', 'system', `Test DM created with ${targetName}. Check your DMs.`); broadcastToStore(channelId, { sender: 'System', content: `Test DM created with ${targetName}.`, senderType: 'system' }); } else { addMessage(channelId, 'System', 'system', `Failed to create DM with ${targetName}.`); broadcastToStore(channelId, { sender: 'System', content: `Failed to create DM with ${targetName}.`, senderType: 'system' }); } } else { // Original commands (unchanged) if (cmd === 'ground') { const groundMsg = { sender: 'System', content: 'GROUND: All agents respond.' }; addMessage(channelId, 'System', 'system', groundMsg.content); broadcastToStore(channelId, groundMsg); const agentsInChannel = Array.from(agents.values()).filter(a => a.channels.includes(channel.name) && !a.isEmbedOperator); for (const agent of agentsInChannel) agentRespond(agent, channelId, groundMsg, false); } else if (cmd === 'research' && args.length) { stopLoop(channelId); channel.researchActive = true; channel.researchTopic = args.join(' '); addMessage(channelId, 'System', 'system', `Research mode started on: ${channel.researchTopic}`); broadcastToStore(channelId, { sender: 'System', content: `Research mode started on: ${channel.researchTopic}`, senderType: 'system' }); scheduleLoopRound(channelId); } else if (cmd === 'abstract') { stopLoop(channelId); channel.abstractActive = true; addMessage(channelId, 'System', 'system', 'Abstract mode active – agents will plan actions.'); broadcastToStore(channelId, { sender: 'System', content: 'Abstract mode active – agents will plan actions.', senderType: 'system' }); scheduleLoopRound(channelId); } else if (cmd === 'plan' && args.length) { stopLoop(channelId); const newState = { active: true, title: args.join(' '), goals: [args.join(' ')], nextSteps: [], completedTasks: [], memory: {} }; setProjectState(channelId, newState); channel.abstractActive = true; addMessage(channelId, 'System', 'system', `📋 Project planning started: "${newState.title}".`); broadcastToStore(channelId, { sender: 'System', content: `Project planning started: "${newState.title}"`, senderType: 'system' }); scheduleLoopRound(channelId); } else if (cmd === 'ralph' && args.length) { stopLoop(channelId); const goal = args.join(' '); loadProjectStateFromLineage(channelId); startRalphLoop(channelId, goal); addMessage(channelId, 'System', 'system', `🧬 **Ralph loop started**\nGoal: ${goal}\nWill converge when similarity ≥ 0.95 or max 30 generations.`); broadcastToStore(channelId, { sender: 'System', content: `Ralph evolution started: ${goal}`, senderType: 'system' }); } else if (cmd === 'stop') { stopLoop(channelId); } else if (cmd === 'list') { const models = await getOllamaModels(); const listText = models.length ? 'Available Ollama models:\n' + models.join('\n') : 'No Ollama models found.'; addMessage(channelId, 'System', 'system', listText); broadcastToStore(channelId, { sender: 'System', content: listText, senderType: 'system' }); } else if (cmd === 'spawn') { ws.send(JSON.stringify({ type: 'models_list', models: await getOllamaModels() })); } else if (cmd === 'siphon') { const topic = args.join(' ') || 'general research topic'; const sessionId = uuidv4(); const session = { id: sessionId, topic, phase: 'Initializing', metric: 0, logs: [], facts: [], notes: [], questions: [], currentQuestionIndex: 0, startedAt: Date.now() }; researchSessions.set(sessionId, session); runResearch(sessionId, topic, channelId).catch(console.error); addMessage(channelId, 'Siphon', 'system', `🔍 Started research on "${topic}". Check #siphon.`); broadcastToStore(channelId, { sender: 'Siphon', content: `Research started: ${topic}`, senderType: 'system' }); } else if (cmd === 'slime') { const token = uuidv4().replace(/-/g, '').substring(0,16); const pin = Math.floor(100000 + Math.random() * 900000).toString(); const expiresAt = Date.now() + 60 * 60 * 1000; slimeSessions.set(token, { pin, expiresAt, channelId }); const url = `http://localhost:${PORT}/slime?token=${token}&pin=${pin}`; addMessage(channelId, 'System', 'system', `📱 Mobile URL: ${url} PIN: ${pin} (expires 1h)`); broadcastToStore(channelId, { sender: 'System', content: `Mobile access: ${url}`, senderType: 'system' }); } else if (cmd === 'pull' && args.length) { const session = researchSessions.get(args[0]); if (!session) { addMessage(channelId, 'System', 'system', `No session ${args[0]}.`); broadcastToStore(channelId, { sender: 'System', content: `No session ${args[0]}.`, senderType: 'system' }); return; } let summary = `📊 **Research "${session.topic}"**\nMetric: ${(session.metric*100).toFixed(0)}%\n`; if (session.notes.length) { const last = session.notes[session.notes.length-1]; summary += `**Latest answer:** ${last.answer.substring(0,300)}\nKey facts:\n${last.facts.slice(0,3).map(f => `- ${f}`).join('\n')}`; } else { summary += 'Research still in progress.'; } addMessage(channelId, 'Siphon', 'system', summary); broadcastToStore(channelId, { sender: 'Siphon', content: summary, senderType: 'system' }); } else if (cmd === 'dm') { const dm = await handleDMCommand(getUserId(ws), args, ws); if (dm) ws.send(JSON.stringify({ type: 'switch_to_dm', dmId: dm.id })); } else if (cmd === 'thread') { const messageId = args[0]; if (!messageId) ws.send(JSON.stringify({ type: 'error', message: 'Usage: /thread ' })); else ws.send(JSON.stringify({ type: 'thread_messages', storeId: channelId, threadId: messageId, messages: getThreadMessages(channelId, messageId) })); } else if (cmd === 'pin') { if (!args[0]) ws.send(JSON.stringify({ type: 'error', message: 'Usage: /pin ' })); else { if (!pinnedMessages.has(channelId)) pinnedMessages.set(channelId, new Set()); pinnedMessages.get(channelId).add(args[0]); ws.send(JSON.stringify({ type: 'pinned', messageId: args[0], channelId })); } } else if (cmd === 'graph') { ws.send(JSON.stringify({ type: 'graph_ack' })); } else if (cmd === 'errorlog') { let logText = '**ERROR LOG**\n'; const errors = global.errorLog || []; errors.slice(0,50).forEach(e => { logText += `${new Date(e.timestamp).toLocaleString()} | ${e.agentId || 'system'}: ${e.error}\n`; }); if (!errors.length) logText += 'No errors recorded.'; addMessage(channelId, 'System', 'system', logText); broadcastToStore(channelId, { sender: 'System', content: logText, senderType: 'system' }); } else if (cmd === 'convergence') { const lineage = reconstructLineage(channelId); let lastSpec = null, sim = 0; for (let i = lineage.length-1; i >= 0; i--) { if (lineage[i].type === 'project_state') { const spec = computeSpecFromState(lineage[i].state); if (lastSpec) { sim = similarity(lastSpec, spec); break; } lastSpec = spec; } } const msg = `🔍 Convergence similarity: ${(sim*100).toFixed(1)}%`; addMessage(channelId, 'System', 'system', msg); broadcastToStore(channelId, { sender: 'System', content: msg, senderType: 'system' }); } else { addMessage(channelId, 'System', 'system', `Unknown command: ${cmd}. Type /help`); broadcastToStore(channelId, { sender: 'System', content: `Unknown command: ${cmd}`, senderType: 'system' }); } } return; } const relevantAgents = Array.from(agents.values()).filter(a => a.channels.includes(channel.name) && !a.isEmbedOperator); const state = getProjectState(channelId); const usePlanning = state.active || channel.abstractActive || channel.researchActive; for (const agent of relevantAgents) { if (ralphActive.get(channelId)) continue; if (usePlanning) await agentPlanAndAct(agent, channelId, messageObj, messageObj.parentId); else await agentRespond(agent, channelId, messageObj, false, messageObj.parentId); } } async function onHumanDmMessage(dmId, messageObj, ws) { const dm = dms.get(dmId); if (!dm) return; const content = messageObj.content; if (content.startsWith('/')) { const parts = content.slice(1).split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); if (cmd === 'ralph' && args.length) { stopRalphLoop(dmId); const goal = args.join(' '); loadProjectStateFromLineage(dmId); startRalphLoop(dmId, goal); addMessage(dmId, 'System', 'system', `🧬 Ralph loop started in DM.\nGoal: ${goal}`); broadcastToStore(dmId, { sender: 'System', content: `Ralph started: ${goal}`, senderType: 'system' }); return; } else if (cmd === 'stop') { stopRalphLoop(dmId); setProjectState(dmId, { active: false, title: null, goals: [], nextSteps: [], completedTasks: [], memory: {} }); addMessage(dmId, 'System', 'system', 'Autonomous mode stopped.'); broadcastToStore(dmId, { sender: 'System', content: 'Autonomous mode stopped.', senderType: 'system' }); return; } else if (cmd === 'help') { const help = 'DM Commands: /ralph , /stop, /help, /tools, /stack ...'; addMessage(dmId, 'System', 'system', help); broadcastToStore(dmId, { sender: 'System', content: help, senderType: 'system' }); return; } } const agentParticipants = dm.participants.filter(p => agents.has(p) && !agents.get(p).isEmbedOperator); const state = getProjectState(dmId); for (const agentId of agentParticipants) { const agent = agents.get(agentId); if (!agent) continue; if (ralphActive.get(dmId)) continue; const usePlanning = state.active; if (usePlanning) await agentPlanAndAct(agent, dmId, messageObj, messageObj.parentId); else await agentRespond(agent, dmId, messageObj, false, messageObj.parentId); } } ''' # end of SERVER_JS # ---------------------------------------------------------------------- # Full HTML Frontend (unchanged) # ---------------------------------------------------------------------- INDEX_HTML = r''' LACK v3.9.2
Agents: 0 🧬 Ralph active
🌓
#general
Thread
LACK · Real‑time scrolling graph | Click agent → edit | Double‑click → DMCONNECTED
''' CONFIG_JSON = r'''{ "httpPort": 3721, "agents": [ { "id": "agent1", "name": "Agent 1", "model": "qwen2.5:0.5b", "systemPrompt": "You are a helpful AI assistant.", "channels": ["general","random","siphon","code"] }, { "id": "agent2", "name": "Agent 2", "model": "qwen2.5:0.5b", "systemPrompt": "You are a creative AI.", "channels": ["general","random","siphon","code"] } ], "channels": [ { "id": "general", "name": "general" }, { "id": "random", "name": "random" }, { "id": "siphon", "name": "siphon" }, { "id": "code", "name": "code" } ], "dms": [] }''' BIN_LACK_JS = r'''#!/usr/bin/env node const { spawn } = require('child_process'); const path = require('path'); const projectRoot = path.resolve(__dirname, '..'); process.chdir(projectRoot); async function checkOllama() { const http = require('http'); return new Promise((resolve) => { const req = http.get('http://localhost:11434/api/tags', (res) => resolve(res.statusCode === 200)); req.on('error', () => resolve(false)); req.setTimeout(1000, () => resolve(false)); }); } async function main() { console.log('\x1b[36m[ LACK v3.9.2 ] Starting – STACK + tools + code moderator\x1b[0m'); if (!await checkOllama()) { console.error('\x1b[31m✗ Ollama not running\x1b[0m'); process.exit(1); } console.log('\x1b[32m✓ Ollama detected\x1b[0m'); const server = spawn('node', ['server.js'], { stdio: 'inherit', cwd: projectRoot }); server.on('error', (err) => { console.error('Failed to start server:', err); process.exit(1); }); process.on('SIGINT', () => { server.kill('SIGINT'); process.exit(); }); } main(); ''' # ---------------------------------------------------------------------- # Python Launcher # ---------------------------------------------------------------------- def create_directory(path): Path(path).mkdir(parents=True, exist_ok=True) def write_file(path, content): with open(path, 'w', encoding='utf-8') as f: f.write(content) def make_executable(path): st = os.stat(path) os.chmod(path, st.st_mode | stat.S_IEXEC) def run_command(cmd, cwd=None): print(f"Running: {' '.join(cmd)}") result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) if result.returncode != 0: print("STDERR:", result.stderr) if "npm" in cmd[0] and "install" in cmd: print("\n❌ npm install failed. Possible fixes:") print(" 1. Ensure Node.js >= 18 is installed: node --version") print(" 2. Check internet connection (npm needs network)") print(" 3. Run manually: npm install express ws uuid axios cheerio simple-git") raise subprocess.CalledProcessError(result.returncode, cmd, output=result.stdout, stderr=result.stderr) print(result.stdout) def open_browser(): time.sleep(2) webbrowser.open('http://localhost:3721') def check_ollama_with_retry(max_attempts=3, delay=2): import urllib.request for attempt in range(1, max_attempts + 1): try: req = urllib.request.Request("http://localhost:11434/api/tags", method="GET") with urllib.request.urlopen(req, timeout=3) as resp: if resp.status == 200: return json.loads(resp.read().decode()) except Exception as e: if attempt < max_attempts: print(f" Ollama check attempt {attempt}/{max_attempts} failed ({e}). Retrying in {delay}s...") time.sleep(delay) else: raise return None def main(): print("=== LACK v3.9.2 – STACK + Code Moderation (enhanced for small models) ===") print("Added JSON repair, forced code blocks, fallbacks, forced commits, test DM.\n") for d in ["config", "public", "bin", "logs", "lineage", "research", "workspace", "lack_repos/templates", "thread_repos"]: create_directory(d) print("Generating files...") write_file("server.js", SERVER_JS) write_file("public/index.html", INDEX_HTML) write_file("config/lack.config.json", CONFIG_JSON) write_file("bin/lack.js", BIN_LACK_JS) make_executable("bin/lack.js") try: node_version = subprocess.run(["node", "--version"], capture_output=True, text=True, check=True) print(f"Node.js detected: {node_version.stdout.strip()}") except (subprocess.CalledProcessError, FileNotFoundError): print("Error: Node.js is not installed. Install from https://nodejs.org") sys.exit(1) if not Path("node_modules").exists(): print("Installing npm dependencies...") run_command(["npm", "install", "express", "ws", "uuid", "axios", "cheerio", "simple-git"]) else: print("node_modules already present.") print("Checking Ollama...") try: models_data = check_ollama_with_retry() if models_data is not None: print("✓ Ollama is running.") model_names = [m['name'] for m in models_data.get('models', [])] if not any('qwen2.5:0.5b' in m for m in model_names): print("⚠ qwen2.5:0.5b not found. Run: ollama pull qwen2.5:0.5b") if not any('nomic-embed-text' in m for m in model_names): print("⚠ nomic-embed-text not found. Run: ollama pull nomic-embed-text") else: print("⚠ Ollama responded but returned no data.") except Exception as e: print(f"⚠ Ollama not reachable after 3 attempts: {e}") threading.Thread(target=open_browser, daemon=True).start() print("\nStarting LACK v3.9.2 with enhanced small‑model resilience. Press Ctrl+C to stop.\n") while True: print("Launching Node server...") server_process = subprocess.Popen( ["node", "server.js"], stdout=sys.stdout, stderr=sys.stderr ) try: server_process.wait() except KeyboardInterrupt: print("Shutting down...") server_process.terminate() sys.exit(0) print("Server crashed. Restarting in 3 seconds...") time.sleep(3) if __name__ == "__main__": main()