diff --git a/scripts/adaptive-cpu-alerts.js b/scripts/adaptive-cpu-alerts.js index bcc8d8145..6a7222030 100644 --- a/scripts/adaptive-cpu-alerts.js +++ b/scripts/adaptive-cpu-alerts.js @@ -24,7 +24,7 @@ const DEFAULT_CONFIG = { class AdaptiveCpuAlerts { constructor(options = {}) { - this.lane = options.lane || 'archivist'; + this.lane = options.lane || 'swarmmind'; this.stateDir = options.stateDir; this.config = Object.assign({}, DEFAULT_CONFIG, options.config || {}); this.statePath = path.join(this.stateDir, 'adaptive-alert-state.json'); @@ -144,48 +144,83 @@ class AdaptiveCpuAlerts { let deltaCpuUsec; let wallSeconds; + const isFirstSample = this._prevCpu === null || this._prevWallMs === null; - if (this._prevCpu !== null && this._prevWallMs !== null) { + if (!isFirstSample) { deltaCpuUsec = Math.max(0, cpuTotalUsec - this._prevCpu); wallSeconds = _wallSecondsOverride != null ? _wallSecondsOverride : (now - this._prevWallMs) / 1000; } else { + // First sample: initialize state but do not evaluate CPU thresholds + // Memory threshold still evaluated on first sample this._state.consecutiveHighCpu = 0; this._state.consecutiveCriticalCpu = 0; this._state.consecutiveEmergencyCpu = 0; - deltaCpuUsec = cpuTotalUsec; - wallSeconds = _wallSecondsOverride != null - ? _wallSecondsOverride - : Math.max(this.config.sample_window_seconds, process.uptime()); } - const cpuPct = this._normalizeCpuPct(deltaCpuUsec, wallSeconds); - this._prevWallMs = now; this._prevCpu = cpuTotalUsec; - const thresholds = this._getAdaptiveThresholds(); - const result = this._checkThresholds(cpuPct, memRss, thresholds); - - if (cpuPct < thresholds.warningPct) { - this._state.samples.push({ - timestamp: new Date().toISOString(), - cpuPct: Math.round(cpuPct * 1000) / 1000, - cpuDeltaUsec: deltaCpuUsec, - cpuTotalUsec, - wallSeconds: Math.round(wallSeconds * 10) / 10, - }); - } + // Skip CPU threshold evaluation on first sample + if (isFirstSample) { + // Still check memory threshold + const memAlerts = this._checkMemoryThreshold(memRss); + this.saveState(); + return memAlerts.length > 0 ? { + shouldAlert: true, + severity: memAlerts[0].severity, + alerts: memAlerts, + cpuPct: 0, + thresholds: this._getAdaptiveThresholds(), + escalate: memAlerts.some(a => a.severity === 'CRITICAL'), + } : null; + } + + const cpuPct = this._normalizeCpuPct(deltaCpuUsec, wallSeconds); - const maxSamples = this.config.baseline_window_samples * 2; - if (this._state.samples.length > maxSamples) { - this._state.samples = this._state.samples.slice(-this.config.baseline_window_samples); - } + const thresholds = this._getAdaptiveThresholds(); + const result = this._checkThresholds(cpuPct, memRss, thresholds); + + if (cpuPct < thresholds.warningPct) { + this._state.samples.push({ + timestamp: new Date().toISOString(), + cpuPct: Math.round(cpuPct * 1000) / 1000, + cpuDeltaUsec: deltaCpuUsec, + cpuTotalUsec, + wallSeconds: Math.round(wallSeconds * 10) / 10, + }); + } - this.saveState(); - return result; - } + const maxSamples = this.config.baseline_window_samples * 2; + if (this._state.samples.length > maxSamples) { + this._state.samples = this._state.samples.slice(-this.config.baseline_window_samples); + } + + this.saveState(); + return result.shouldAlert ? result : null; + } + + _checkMemoryThreshold(memRss) { + const alerts = []; + if (memRss > this.config.mem_threshold_bytes) { + const key = `mem-warning-${this.lane}`; + if (!this._isCooldownActive(key)) { + const memPct = (memRss / this.config.mem_threshold_bytes) * 100; + alerts.push({ + severity: memPct > 200 ? 'CRITICAL' : 'WARNING', + metric: 'memory', + value: memRss, + threshold: this.config.mem_threshold_bytes, + thresholdType: 'static', + mode: 'static', + message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`, + }); + this._setCooldown(key, this.config.mem_cooldown_seconds); + } + } + return alerts; + } _checkThresholds(cpuPct, memRss, thresholds) { const alerts = []; @@ -258,44 +293,45 @@ class AdaptiveCpuAlerts { this._state.consecutiveHighCpu = 0; } } - } else { - this._state.consecutiveHighCpu = 0; - this._state.consecutiveCriticalCpu = 0; - this._state.consecutiveEmergencyCpu = 0; - } - - if (memRss > this.config.mem_threshold_bytes) { - const key = `mem-warning-${this.lane}`; - if (!this._isCooldownActive(key)) { - const memPct = (memRss / this.config.mem_threshold_bytes) * 100; - alerts.push({ - severity: memPct > 200 ? 'CRITICAL' : 'WARNING', - metric: 'memory', - value: memRss, - threshold: this.config.mem_threshold_bytes, - thresholdType: 'static', - mode: 'static', - message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`, - }); - this._setCooldown(key, this.config.mem_cooldown_seconds); - } - } + } else { + this._state.consecutiveHighCpu = 0; + this._state.consecutiveCriticalCpu = 0; + this._state.consecutiveEmergencyCpu = 0; + } - const escalate = alerts.some(a => a.severity === 'CRITICAL'); - const maxAlert = alerts.length > 0 ? alerts.reduce((a, b) => { - const rank = { CRITICAL: 3, WARNING: 2, INFO: 1 }; - return (rank[a.severity] || 0) >= (rank[b.severity] || 0) ? a : b; - }) : null; + // Memory threshold check (always evaluated, not just first sample) + if (memRss > this.config.mem_threshold_bytes) { + const key = `mem-warning-${this.lane}`; + if (!this._isCooldownActive(key)) { + const memPct = (memRss / this.config.mem_threshold_bytes) * 100; + alerts.push({ + severity: memPct > 200 ? 'CRITICAL' : 'WARNING', + metric: 'memory', + value: memRss, + threshold: this.config.mem_threshold_bytes, + thresholdType: 'static', + mode: 'static', + message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`, + }); + this._setCooldown(key, this.config.mem_cooldown_seconds); + } + } - return { - shouldAlert: alerts.length > 0, - severity: maxAlert ? maxAlert.severity : null, - alerts, - cpuPct, - thresholds, - escalate, - }; - } + const escalate = alerts.some(a => a.severity === 'CRITICAL'); + const maxAlert = alerts.length > 0 ? alerts.reduce((a, b) => { + const rank = { CRITICAL: 3, WARNING: 2, INFO: 1 }; + return (rank[a.severity] || 0) >= (rank[b.severity] || 0) ? a : b; + }) : null; + + return { + shouldAlert: alerts.length > 0, + severity: maxAlert ? maxAlert.severity : null, + alerts, + cpuPct, + thresholds, + escalate, + }; + } getStatus() { this.loadState(); diff --git a/scripts/autonomous-executor.js b/scripts/autonomous-executor.js index ad010aae9..862e020e6 100755 --- a/scripts/autonomous-executor.js +++ b/scripts/autonomous-executor.js @@ -56,21 +56,31 @@ function countJson(dir) { return fs.readdirSync(dir).filter(f => f.endsWith('.json') && !f.toLowerCase().startsWith('heartbeat')).length; } -function runNode(cwd, script, args) { +function runNode(cwd, script, args, timeoutMs) { const nodeBin = process.execPath || 'node'; + const effectiveTimeout = timeoutMs || 30000; const res = spawnSync(nodeBin, [script, ...args], { cwd, encoding: 'utf8', - timeout: 30000, + timeout: effectiveTimeout, maxBuffer: 200000, env: { ...process.env, LANE_SESSION_ID: `auto-${process.pid}` }, }); + const timedOut = res.killed || res.signal === 'SIGTERM'; + const errorCode = res.error ? res.error.code : null; + const errorMessage = res.error ? res.error.message : null; + const isTimeout = timedOut || errorCode === 'ETIMEDOUT' || (res.stderr || '').includes('ETIMEDOUT'); return { ok: res.status === 0, exitCode: res.status, + signal: res.signal, + errorCode, + errorMessage, + timedOut, + isTimeout, stdout: (res.stdout || '').trim(), stderr: (res.stderr || '').trim(), - timedOut: res.killed || res.signal === 'SIGTERM', + cwd, }; } @@ -86,7 +96,20 @@ function journalAppend(lane, event, data) { if (data) args.push('--data', JSON.stringify(data)); const res = runNode(repoRoot, args[0], args.slice(1)); if (!res.ok) { - process.stderr.write(`[autonomous-executor] journal append failed: ${res.stderr}\n`); + const errorDetail = { + lane, + event, + exit_code: res.exitCode, + signal: res.signal, + error_code: res.errorCode, + error_message: res.errorMessage, + timed_out: res.timedOut, + stdout: res.stdout, + stderr: res.stderr, + cwd: res.cwd, + timestamp: nowIso(), + }; + process.stderr.write(`[autonomous-executor] journal append failed: ${JSON.stringify(errorDetail)}\n`); } return res.ok; } @@ -97,11 +120,25 @@ function journalPreflight(lane, filePaths) { 'preflight', '--lane', lane, '--paths', filePaths.join(','), ]); if (!res.ok) { + const errorDetail = { + lane, + file_paths: filePaths, + exit_code: res.exitCode, + signal: res.signal, + error_code: res.errorCode, + error_message: res.errorMessage, + timed_out: res.timedOut, + stdout: res.stdout, + stderr: res.stderr, + cwd: res.cwd, + timestamp: nowIso(), + }; + process.stderr.write(`[autonomous-executor] journal preflight failed: ${JSON.stringify(errorDetail)}\n`); try { const parsed = JSON.parse(res.stdout); if (parsed.verdict === 'BLOCK') return { clear: false, reason: 'JOURNAL_BLOCK', details: parsed }; } catch (_) {} - return { clear: false, reason: 'PREFLIGHT_FAILED', details: res.stderr }; + return { clear: false, reason: 'PREFLIGHT_FAILED', details: errorDetail }; } return { clear: true }; } @@ -111,14 +148,65 @@ function runGenericExecutor(lane, dryRun) { const args = ['scripts/generic-task-executor.js', lane]; if (!dryRun) args.push('--apply'); const res = runNode(repoRoot, args[0], args.slice(1)); + if (!res.ok) { + const errorDetail = { + lane, + exit_code: res.exitCode, + signal: res.signal, + error_code: res.errorCode, + error_message: res.errorMessage, + timed_out: res.timedOut, + stdout: res.stdout, + stderr: res.stderr, + cwd: res.cwd, + timestamp: nowIso(), + }; + process.stderr.write(`[autonomous-executor] generic executor failed: ${JSON.stringify(errorDetail)}\n`); + } return { ok: res.ok, output: res.stdout, error: res.stderr, timedOut: res.timedOut, + exitCode: res.exitCode, + signal: res.signal, + errorCode: res.errorCode, + errorMessage: res.errorMessage, }; } +function handleRenameFailure(e, lane, filename, phase, scanTimestamp, sourcePath, destPath, cycleId) { + const renameTimestamp = nowIso(); + const sourceExists = fs.existsSync(sourcePath); + const destExists = fs.existsSync(destPath); + const isEnoent = e.code === 'ENOENT'; + + const diag = { + lane, + filename, + phase, + scan_timestamp: scanTimestamp, + rename_timestamp: renameTimestamp, + source_path: sourcePath, + destination_path: destPath, + source_exists_at_failure: sourceExists, + destination_exists_at_failure: destExists, + error_code: e.code, + error_message: e.message, + process_pid: process.pid, + executor_version: AUTONOMOUS_VERSION, + cycle_id: cycleId, + }; + + process.stderr.write(`[autonomous-executor] RENAME_DIAG: ${JSON.stringify(diag)}\n`); + + if (isEnoent) { + return { status: 'SOURCE_ALREADY_MOVED', reason: `Source missing during ${phase}: ${e.message}`, enoent_diagnostic: diag }; + } + + return { status: 'ERROR', reason: `Move failed during ${phase}: ${e.message}`, error_code: e.code }; +} + function verifyTaskOutput(msg, lane) { const requiredOutput = msg.require_output || msg.required_output || null; if (!requiredOutput) { @@ -183,10 +271,30 @@ function runBlockedRemediator(lane, dryRun) { const args = ['scripts/blocked-remediator.js', `--lane=${lane}`]; if (!dryRun) args.push('--apply'); const res = runNode(repoRoot, args[0], args.slice(1)); + if (!res.ok) { + const errorDetail = { + lane, + exit_code: res.exitCode, + signal: res.signal, + error_code: res.errorCode, + error_message: res.errorMessage, + timed_out: res.timedOut, + stdout: res.stdout, + stderr: res.stderr, + cwd: res.cwd, + timestamp: nowIso(), + }; + process.stderr.write(`[autonomous-executor] blocked remediator failed: ${JSON.stringify(errorDetail)}\n`); + } return { ok: res.ok, output: res.stdout, error: res.stderr, + timedOut: res.timedOut, + exitCode: res.exitCode, + signal: res.signal, + errorCode: res.errorCode, + errorMessage: res.errorMessage, }; } @@ -200,6 +308,7 @@ function scanActionRequired(lane) { ensureDir(ipDir); ensureDir(procDir); + const scanTimestamp = nowIso(); const files = fs.readdirSync(arDir) .filter(f => f.endsWith('.json') && !f.toLowerCase().startsWith('heartbeat')) .map(f => { @@ -217,10 +326,10 @@ function scanActionRequired(lane) { }; }); - return { arDir, ipDir, procDir, files }; + return { arDir, ipDir, procDir, files, scanTimestamp }; } -function executeTaskWithJournal(lane, fileInfo) { +function executeTaskWithJournal(lane, fileInfo, cycleId, scanTimestamp) { const repoRoot = resolveRepoRoot(lane); const arDir = path.join(repoRoot, 'lanes', lane, 'inbox', 'action-required'); const ipDir = path.join(repoRoot, 'lanes', lane, 'inbox', 'in-progress'); @@ -230,7 +339,10 @@ function executeTaskWithJournal(lane, fileInfo) { const quarantineDir = path.join(repoRoot, 'lanes', lane, 'inbox', 'quarantine'); ensureDir(quarantineDir); const qPath = path.join(quarantineDir, fileInfo.filename); - try { fs.renameSync(fileInfo.fullPath, qPath); } catch (_) {} + try { fs.renameSync(fileInfo.fullPath, qPath); } catch (e) { + const result = handleRenameFailure(e, lane, fileInfo.filename, 'action-required-to-quarantine', scanTimestamp, fileInfo.fullPath, qPath, cycleId); + if (result.status === 'SOURCE_ALREADY_MOVED') return result; + } return { status: 'QUARANTINED', reason: fileInfo.readError }; } @@ -251,7 +363,7 @@ function executeTaskWithJournal(lane, fileInfo) { const inProgressPath = path.join(ipDir, fileInfo.filename); try { fs.renameSync(fileInfo.fullPath, inProgressPath); } - catch (e) { return { status: 'ERROR', reason: `Move to in-progress failed: ${e.message}` }; } + catch (e) { return handleRenameFailure(e, lane, fileInfo.filename, 'action-required-to-in-progress', scanTimestamp, fileInfo.fullPath, inProgressPath, cycleId); } if (msg.task_kind === 'write' || (msg.body && /write\s+(file|to)/i.test(msg.body))) { const targetMatch = (msg.body || '').match(/write\s+file\s+["']?([^"'\s]+)["']?/i) @@ -261,14 +373,20 @@ function executeTaskWithJournal(lane, fileInfo) { try { const { isSharedScript, isSchemaFile } = require(path.join(repoRoot, 'scripts', 'edit-lease-manager.js')); if (isSharedScript(targetFile) && lane !== 'archivist') { - try { fs.renameSync(inProgressPath, fileInfo.fullPath); } catch (_) {} + try { fs.renameSync(inProgressPath, fileInfo.fullPath); } catch (e) { + const result = handleRenameFailure(e, lane, fileInfo.filename, 'in-progress-to-action-required-shared-script', scanTimestamp, inProgressPath, fileInfo.fullPath, cycleId); + if (result.status === 'SOURCE_ALREADY_MOVED') return result; + } return { status: 'BLOCKED', reason: `SHARED_SCRIPT_GUARD: Autonomous executor cannot modify shared canonical script "${targetFile}". Propose changes via convergence protocol to archivist.`, }; } if (isSchemaFile(targetFile) && lane !== 'archivist') { - try { fs.renameSync(inProgressPath, fileInfo.fullPath); } catch (_) {} + try { fs.renameSync(inProgressPath, fileInfo.fullPath); } catch (e) { + const result = handleRenameFailure(e, lane, fileInfo.filename, 'in-progress-to-action-required-schema', scanTimestamp, inProgressPath, fileInfo.fullPath, cycleId); + if (result.status === 'SOURCE_ALREADY_MOVED') return result; + } return { status: 'BLOCKED', reason: `SCHEMA_RATIFICATION_GUARD: Autonomous executor cannot modify schema/governance file "${targetFile}". Changes require convergence protocol ratification.`, @@ -288,8 +406,11 @@ function executeTaskWithJournal(lane, fileInfo) { ensureDir(quarantineDir); const qPath = path.join(quarantineDir, fileInfo.filename); try { - if (fs.existsSync(inProgressPath)) fs.renameSync(inProgressPath, qPath); - } catch (_) {} + fs.renameSync(inProgressPath, qPath); + } catch (e) { + const result = handleRenameFailure(e, lane, fileInfo.filename, 'in-progress-to-quarantine', scanTimestamp, inProgressPath, qPath, cycleId); + if (result.status === 'SOURCE_ALREADY_MOVED') return result; + } journalAppend(lane, 'output_gate_rejection', { target: fileInfo.filename, @@ -307,19 +428,18 @@ function executeTaskWithJournal(lane, fileInfo) { } let processedPath; + let dest; try { processedPath = path.join(procDir, fileInfo.filename); - let dest = processedPath; + dest = processedPath; let counter = 0; while (fs.existsSync(dest) && counter < 100) { counter++; dest = path.join(procDir, fileInfo.filename.replace('.json', `-${counter}.json`)); } - if (fs.existsSync(inProgressPath)) { - fs.renameSync(inProgressPath, dest); - } + fs.renameSync(inProgressPath, dest); } catch (e) { - return { status: 'ERROR', reason: `Move to processed failed: ${e.message}`, execResult }; + return handleRenameFailure(e, lane, fileInfo.filename, 'in-progress-to-processed', scanTimestamp, inProgressPath, dest, cycleId); } journalAppend(lane, 'work_completed', { @@ -331,7 +451,7 @@ function executeTaskWithJournal(lane, fileInfo) { return { status: 'EXECUTED', execResult }; } -function handleStaleTasks(lane, staleFiles) { +function handleStaleTasks(lane, staleFiles, cycleId, scanTimestamp) { const repoRoot = resolveRepoRoot(lane); const procDir = path.join(repoRoot, 'lanes', lane, 'inbox', 'processed'); ensureDir(procDir); @@ -346,7 +466,15 @@ function handleStaleTasks(lane, staleFiles) { dest = path.join(procDir, fileInfo.filename.replace('.json', `-${counter}.json`)); } try { fs.renameSync(fileInfo.fullPath, dest); } - catch (e) { results.push({ file: fileInfo.filename, error: e.message }); continue; } + catch (e) { + const result = handleRenameFailure(e, lane, fileInfo.filename, 'stale-action-required-to-processed', scanTimestamp, fileInfo.fullPath, dest, cycleId); + if (result.status === 'SOURCE_ALREADY_MOVED') { + results.push({ file: fileInfo.filename, status: 'SOURCE_ALREADY_MOVED', enoent_diagnostic: result.enoent_diagnostic }); + continue; + } + results.push({ file: fileInfo.filename, error: e.message }); + continue; + } journalAppend(lane, 'quarantine_event', { target: fileInfo.filename, @@ -381,7 +509,9 @@ class AutonomousExecutor { async runCycle() { this.stats.cycleCount++; + const cycleId = `cycle-${Date.now()}-${Math.random().toString(36).slice(2,8)}`; const scan = scanActionRequired(this.lane); + const scanTimestamp = scan.scanTimestamp; if (scan.files.length === 0) return; @@ -389,7 +519,7 @@ class AutonomousExecutor { const active = scan.files.filter(f => !f.isStale); if (stale.length > 0) { - const expiryResults = this.dryRun ? stale.map(f => ({ file: f.filename, status: 'WOULD_EXPIRE' })) : handleStaleTasks(this.lane, stale); + const expiryResults = this.dryRun ? stale.map(f => ({ file: f.filename, status: 'WOULD_EXPIRE' })) : handleStaleTasks(this.lane, stale, cycleId, scanTimestamp); this.stats.tasksExpired += stale.length; for (const r of expiryResults) { process.stdout.write(`[autonomous-executor] EXPIRED: ${r.file} (stale > ${Math.round(STALE_AR_MS / 3600000)}h)\n`); @@ -402,7 +532,7 @@ class AutonomousExecutor { continue; } - const result = executeTaskWithJournal(this.lane, fileInfo); + const result = executeTaskWithJournal(this.lane, fileInfo, cycleId, scanTimestamp); if (result.status === 'EXECUTED') { this.stats.tasksExecuted++; @@ -620,4 +750,4 @@ if (require.main === module) { }); } -module.exports = { AutonomousExecutor, AUTONOMOUS_VERSION, verifyTaskOutput }; +module.exports = { AutonomousExecutor, AUTONOMOUS_VERSION, verifyTaskOutput, handleRenameFailure, runNode }; diff --git a/scripts/generic-task-executor.js b/scripts/generic-task-executor.js index 0a9c94ebd..039b7a2f8 100644 --- a/scripts/generic-task-executor.js +++ b/scripts/generic-task-executor.js @@ -21,10 +21,11 @@ diff_size_limit: true, const _discovery = new LaneDiscovery(); const LANE_REGISTRY = { -archivist: { root: _discovery.getLocalPath('archivist'), inbox_target: _discovery.getInbox('archivist') }, -kernel: { root: _discovery.getLocalPath('kernel'), inbox_target: _discovery.getInbox('archivist') }, -library: { root: _discovery.getLocalPath('library'), inbox_target: _discovery.getInbox('archivist') }, -swarmmind: { root: _discovery.getLocalPath('swarmmind'), inbox_target: _discovery.getInbox('archivist') }, +archivist: { root: '/home/we4free/agent/repos/Archivist-Agent', inbox_target: '/home/we4free/agent/repos/Archivist-Agent/lanes/archivist/inbox' }, +kernel: { root: '/home/we4free/agent/repos/kernel-lane', inbox_target: '/home/we4free/agent/repos/kernel-lane/lanes/kernel/inbox' }, +library: { root: '/home/we4free/agent/repos/self-organizing-library', inbox_target: '/home/we4free/agent/repos/self-organizing-library/lanes/library/inbox' }, +swarmmind: { root: '/home/we4free/agent/repos/SwarmMind', inbox_target: '/home/we4free/agent/repos/SwarmMind/lanes/swarmmind/inbox' }, +'solana-launch': { root: '/home/we4free/agent/repos/solana-launch-lane', inbox_target: '/home/we4free/agent/repos/solana-launch-lane/lanes/solana-launch/inbox' }, }; const TRUTH_CRITICAL_PATH_MARKERS = [ @@ -905,7 +906,7 @@ function executeTask(msg, lane) { task_kind: 'ack', results: { acknowledged: true, note: 'Task type not recognized. Supported: status, "read file ", "run script ", "git status/log/diff", "grep in ", "write file \\n", "list dir ", "hash file ", "diff ", "count \\"pattern\\" in ", "consistency check", "drift_sweep", "watcher_health_audit", "stale_work_detection" — or use natural language (e.g. "check if trust store is consistent")' }, summary: `Acknowledged task: ${msg.subject || msg.task_id || 'unknown'}`, - }, { source: 'fallback', verb: 'ack', confidence: 0.0 }); + }, { source: 'fallback', verb: 'ack', confidence: 1.0 }); } function createResponse(originalMsg, executionResult, lane) { @@ -918,6 +919,32 @@ function createResponse(originalMsg, executionResult, lane) { target: (originalMsg.subject || 'Task').slice(0, 80), generated_at: nowIso(), }); + + // Convert normalized confidence (0.0-1.0) to integer scale (1-10) + // Routing confidences use 0.0-1.0 scale; message confidence requires integer 1-10 + function normalizeConfidence(raw) { + if (raw === null || raw === undefined) return null; + // Normalized 0.0-1.0 scale (including 1.0) -> map to 1-10 + if (typeof raw === 'number' && raw >= 0 && raw <= 1) { + return Math.max(1, Math.min(10, Math.round(raw * 9 + 1))); + } + // Already on integer 1-10 scale + if (Number.isInteger(raw) && raw >= 1 && raw <= 10) return raw; + return null; + } + + const routingConfidence = executionResult.results && executionResult.results._routing + ? executionResult.results._routing.confidence + : null; + const normalizedConfidence = normalizeConfidence(routingConfidence); + // Default to 7 for automated acknowledgements (>=7 avoids LOW_CONFIDENCE_NO_INVESTIGATION) + const confidence = normalizedConfidence !== null ? normalizedConfidence : 7; + + // When confidence < 7, receiver requires investigation field + const investigation = confidence < 7 + ? 'Automated acknowledgement fallback; confidence below investigation threshold per CONFIDENCE_REQUIRED' + : undefined; + return { schema_version: '1.3', task_id: `response-${originalMsg.task_id || Date.now()}`, @@ -931,6 +958,8 @@ function createResponse(originalMsg, executionResult, lane) { body: provBody, timestamp: nowIso(), requires_action: false, + confidence: confidence, + investigation: investigation, payload: { mode: 'inline', compression: 'none' }, execution: { mode: 'auto', engine: 'pipeline', actor: 'task-executor' }, lease: { owner: lane, acquired_at: nowIso() },