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:
@@ -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}`,
|
||||
``,
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user