From d1c693fb17fc6b8b43815740096d79edd216d4ae Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Thu, 26 Mar 2026 21:25:30 +0900 Subject: [PATCH] fix: treat signal-killed agents as success when output was delivered Two bugs fixed: 1. agent-runner: process close handler only checked `code !== 0`, but signal kills (SIGTERM/SIGKILL from post-close cleanup) set code=null which was misclassified as error even after successful output delivery. Now checks `code === null && signal` and resolves as success when hadStreamingOutput is true. 2. message-agent-executor: wrappedOnOutput persisted newSessionId before checking for poisoned session, allowing a stale session to be re-saved after clearSession. Reordered to check poison first and guard persist with !resetSessionRequested. Co-Authored-By: Claude Opus 4.6 --- src/agent-runner.ts | 48 ++++++++++++++++++++++++++++++++++- src/message-agent-executor.ts | 10 +++++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 174baaa..cbb9722 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -269,7 +269,7 @@ export async function runAgentProcess( } }); - proc.on('close', (code) => { + proc.on('close', (code, signal) => { clearTimeout(timeout); const duration = Date.now() - startTime; @@ -286,6 +286,7 @@ export async function runAgentProcess( `Process: ${processName}`, `Duration: ${duration}ms`, `Exit Code: ${code}`, + `Signal: ${signal}`, `Had Streaming Output: ${hadStreamingOutput}`, ].join('\n'), ); @@ -299,6 +300,7 @@ export async function runAgentProcess( processName, duration, code, + signal, }, 'Agent timed out after output (idle cleanup)', ); @@ -316,6 +318,49 @@ export async function runAgentProcess( return; } + // Signal kill (SIGTERM/SIGKILL) from post-close cleanup or service + // restart. When the agent already delivered streaming output this is + // normal lifecycle — not an error. + if (code === null && signal) { + if (hadStreamingOutput) { + logger.info( + { + group: group.name, + chatJid: input.chatJid, + runId: input.runId, + processName, + duration, + signal, + }, + 'Agent terminated by signal after output delivery (normal cleanup)', + ); + outputChain.then(() => { + resolve({ status: 'success', result: null, newSessionId }); + }); + return; + } + // No output delivered before signal kill — genuine error + logger.error( + { + group: group.name, + chatJid: input.chatJid, + runId: input.runId, + processName, + duration, + signal, + }, + 'Agent killed by signal before producing output', + ); + outputChain.then(() => { + resolve({ + status: 'error', + result: null, + error: `Agent killed by ${signal} before producing output`, + }); + }); + return; + } + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const logFile = path.join( logsDir, @@ -335,6 +380,7 @@ export async function runAgentProcess( `AgentType: ${group.agentType || 'claude-code'}`, `Duration: ${duration}ms`, `Exit Code: ${code}`, + `Signal: ${signal}`, ``, ]; diff --git a/src/message-agent-executor.ts b/src/message-agent-executor.ts index 2a12dd9..112cf68 100644 --- a/src/message-agent-executor.ts +++ b/src/message-agent-executor.ts @@ -140,9 +140,6 @@ export async function runAgentForGroup( const wrappedOnOutput = onOutput ? async (output: AgentOutput) => { - if (persistSessionIds && output.newSessionId) { - deps.persistSession(group.folder, output.newSessionId); - } if ( persistSessionIds && isClaudeCodeAgent && @@ -150,6 +147,13 @@ export async function runAgentForGroup( ) { resetSessionRequested = true; } + if ( + persistSessionIds && + output.newSessionId && + !resetSessionRequested + ) { + deps.persistSession(group.folder, output.newSessionId); + } const evaluation = evaluateStreamedOutput(output, streamedState, { agentType: isClaudeCodeAgent ? 'claude-code' : 'codex', provider,