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 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-26 21:25:30 +09:00
parent c4744177e9
commit d1c693fb17
2 changed files with 54 additions and 4 deletions

View File

@@ -269,7 +269,7 @@ export async function runAgentProcess(
} }
}); });
proc.on('close', (code) => { proc.on('close', (code, signal) => {
clearTimeout(timeout); clearTimeout(timeout);
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@@ -286,6 +286,7 @@ export async function runAgentProcess(
`Process: ${processName}`, `Process: ${processName}`,
`Duration: ${duration}ms`, `Duration: ${duration}ms`,
`Exit Code: ${code}`, `Exit Code: ${code}`,
`Signal: ${signal}`,
`Had Streaming Output: ${hadStreamingOutput}`, `Had Streaming Output: ${hadStreamingOutput}`,
].join('\n'), ].join('\n'),
); );
@@ -299,6 +300,7 @@ export async function runAgentProcess(
processName, processName,
duration, duration,
code, code,
signal,
}, },
'Agent timed out after output (idle cleanup)', 'Agent timed out after output (idle cleanup)',
); );
@@ -316,6 +318,49 @@ export async function runAgentProcess(
return; 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 timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const logFile = path.join( const logFile = path.join(
logsDir, logsDir,
@@ -335,6 +380,7 @@ export async function runAgentProcess(
`AgentType: ${group.agentType || 'claude-code'}`, `AgentType: ${group.agentType || 'claude-code'}`,
`Duration: ${duration}ms`, `Duration: ${duration}ms`,
`Exit Code: ${code}`, `Exit Code: ${code}`,
`Signal: ${signal}`,
``, ``,
]; ];

View File

@@ -140,9 +140,6 @@ export async function runAgentForGroup(
const wrappedOnOutput = onOutput const wrappedOnOutput = onOutput
? async (output: AgentOutput) => { ? async (output: AgentOutput) => {
if (persistSessionIds && output.newSessionId) {
deps.persistSession(group.folder, output.newSessionId);
}
if ( if (
persistSessionIds && persistSessionIds &&
isClaudeCodeAgent && isClaudeCodeAgent &&
@@ -150,6 +147,13 @@ export async function runAgentForGroup(
) { ) {
resetSessionRequested = true; resetSessionRequested = true;
} }
if (
persistSessionIds &&
output.newSessionId &&
!resetSessionRequested
) {
deps.persistSession(group.folder, output.newSessionId);
}
const evaluation = evaluateStreamedOutput(output, streamedState, { const evaluation = evaluateStreamedOutput(output, streamedState, {
agentType: isClaudeCodeAgent ? 'claude-code' : 'codex', agentType: isClaudeCodeAgent ? 'claude-code' : 'codex',
provider, provider,