Histórico persistente de execuções com visualização detalhada

- Novo executionsStore em db.js com cache in-memory e escrita debounced
- Camada de cache (src/cache/index.js) com TTL e suporte opcional a Redis
- Persistência de execuções de agentes e pipelines com metadados completos
- Pipeline grava cada etapa com prompt, resultado, timestamps e status
- 4 endpoints REST: listagem paginada com filtros, detalhe, exclusão individual e limpeza total
- Componente frontend (history.js) com cards, filtros, paginação e modal de detalhe
- Timeline visual para pipelines com prompts colapsáveis por etapa
- Correção do executor: --max-turns em vez de --max-tokens, --permission-mode bypassPermissions
- Refatoração do scheduler com persistência melhorada e graceful shutdown
This commit is contained in:
Frederico Castro
2026-02-26 01:36:28 -03:00
parent 2f7a9d4c56
commit 4b6c876f36
13 changed files with 1536 additions and 398 deletions

View File

@@ -6,20 +6,23 @@ import { settingsStore } from '../store/db.js';
const CLAUDE_BIN = resolveBin();
const activeExecutions = new Map();
let maxConcurrent = settingsStore.get().maxConcurrent || 5;
export function updateMaxConcurrent(value) {
maxConcurrent = Math.max(1, Math.min(20, parseInt(value) || 5));
}
function resolveBin() {
if (process.env.CLAUDE_BIN) return process.env.CLAUDE_BIN;
const home = process.env.HOME || '';
const candidates = [
`${home}/.local/bin/claude`,
'/usr/local/bin/claude',
'/usr/bin/claude',
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return 'claude';
}
@@ -58,7 +61,6 @@ function buildArgs(agentConfig, prompt) {
}
args.push('--permission-mode', agentConfig.permissionMode || 'bypassPermissions');
return args;
}
@@ -89,13 +91,8 @@ function extractText(event) {
.join('');
}
if (event.type === 'content_block_delta' && event.delta?.text) {
return event.delta.text;
}
if (event.type === 'content_block_start' && event.content_block?.text) {
return event.content_block.text;
}
if (event.type === 'content_block_delta' && event.delta?.text) return event.delta.text;
if (event.type === 'content_block_start' && event.content_block?.text) return event.content_block.text;
if (event.type === 'result') {
if (typeof event.result === 'string') return event.result;
@@ -108,17 +105,10 @@ function extractText(event) {
}
if (event.type === 'text') return event.content || null;
return null;
}
function getMaxConcurrent() {
const s = settingsStore.get();
return s.maxConcurrent || 5;
}
export function execute(agentConfig, task, callbacks = {}) {
const maxConcurrent = getMaxConcurrent();
if (activeExecutions.size >= maxConcurrent) {
const err = new Error(`Limite de ${maxConcurrent} execuções simultâneas atingido`);
if (callbacks.onError) callbacks.onError(err, uuidv4());
@@ -145,9 +135,7 @@ export function execute(agentConfig, task, callbacks = {}) {
spawnOptions.cwd = agentConfig.workingDirectory;
}
console.log(`[executor] Iniciando: ${executionId}`);
console.log(`[executor] Modelo: ${agentConfig.model || 'claude-sonnet-4-6'}`);
console.log(`[executor] cwd: ${spawnOptions.cwd || process.cwd()}`);
console.log(`[executor] Iniciando: ${executionId} | Modelo: ${agentConfig.model || 'claude-sonnet-4-6'}`);
const child = spawn(CLAUDE_BIN, args, spawnOptions);
let hadError = false;
@@ -165,14 +153,12 @@ export function execute(agentConfig, task, callbacks = {}) {
let fullText = '';
child.stdout.on('data', (chunk) => {
const raw = chunk.toString();
const lines = (outputBuffer + raw).split('\n');
const lines = (outputBuffer + chunk.toString()).split('\n');
outputBuffer = lines.pop();
for (const line of lines) {
const parsed = parseStreamLine(line);
if (!parsed) continue;
const text = extractText(parsed);
if (text) {
fullText += text;
@@ -193,7 +179,6 @@ export function execute(agentConfig, task, callbacks = {}) {
});
child.on('close', (code) => {
console.log(`[executor][close] code=${code} hadError=${hadError}`);
activeExecutions.delete(executionId);
if (hadError) return;
@@ -206,15 +191,7 @@ export function execute(agentConfig, task, callbacks = {}) {
}
if (onComplete) {
onComplete(
{
executionId,
exitCode: code,
result: fullText,
stderr: errorBuffer,
},
executionId,
);
onComplete({ executionId, exitCode: code, result: fullText, stderr: errorBuffer }, executionId);
}
});
@@ -224,22 +201,19 @@ export function execute(agentConfig, task, callbacks = {}) {
export function cancel(executionId) {
const execution = activeExecutions.get(executionId);
if (!execution) return false;
execution.process.kill('SIGTERM');
activeExecutions.delete(executionId);
return true;
}
export function cancelAllExecutions() {
for (const [id, exec] of activeExecutions) {
exec.process.kill('SIGTERM');
}
for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM');
activeExecutions.clear();
}
export function getActiveExecutions() {
return Array.from(activeExecutions.entries()).map(([id, exec]) => ({
executionId: id,
return Array.from(activeExecutions.values()).map((exec) => ({
executionId: exec.executionId,
startedAt: exec.startedAt,
agentConfig: exec.agentConfig,
}));