mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-08-27 05:32:43 +02:00
fix: terminate failed scans in Temporal and surface the reason when following (#429)
* fix(cli): skip splash screen off a TTY (e.g. CI) * fix: terminate failed scans in Temporal and surface the reason when following * fix(cli): indent embedded newlines within failure-error segments * fix(worker): omit the Agent Breakdown section when no agents completed * fix(cli): don't reprint the failure reason when the log already showed it * fix(worker): indent embedded newlines within the workflow.log error block
This commit is contained in:
@@ -303,13 +303,17 @@ export class WorkflowLogger {
|
||||
* Output: "Error: phase context\n ErrorType\n ..."
|
||||
*/
|
||||
private formatErrorBlock(errorString: string): string {
|
||||
const segments = errorString.split('|');
|
||||
const label = 'Error: ';
|
||||
const indent = ' '.repeat(label.length);
|
||||
|
||||
const lines = segments.map((segment, i) => (i === 0 ? `${label}${segment.trim()}` : `${indent}${segment.trim()}`));
|
||||
// Segments are delimited by '|'; a segment's own embedded newlines (e.g. a multi-line
|
||||
// validation message) become their own lines so each aligns under the label.
|
||||
const lines = errorString
|
||||
.split(/[|\n]/)
|
||||
.map((segment) => segment.trim())
|
||||
.filter((segment) => segment.length > 0);
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
return `${lines.map((line, i) => (i === 0 ? `${label}${line}` : `${indent}${line}`)).join('\n')}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,17 +340,19 @@ export class WorkflowLogger {
|
||||
lines.push(this.formatErrorBlock(summary.error).trimEnd());
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('Agent Breakdown:');
|
||||
if (summary.completedAgents.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('Agent Breakdown:');
|
||||
|
||||
for (const agentName of summary.completedAgents) {
|
||||
const metrics = summary.agentMetrics[agentName];
|
||||
if (metrics) {
|
||||
const duration = formatDuration(metrics.durationMs);
|
||||
const cost = metrics.costUsd !== null ? `$${metrics.costUsd.toFixed(4)}` : 'N/A';
|
||||
lines.push(` - ${agentName} (${duration}, ${cost})`);
|
||||
} else {
|
||||
lines.push(` - ${agentName}`);
|
||||
for (const agentName of summary.completedAgents) {
|
||||
const metrics = summary.agentMetrics[agentName];
|
||||
if (metrics) {
|
||||
const duration = formatDuration(metrics.durationMs);
|
||||
const cost = metrics.costUsd !== null ? `$${metrics.costUsd.toFixed(4)}` : 'N/A';
|
||||
lines.push(` - ${agentName} (${duration}, ${cost})`);
|
||||
} else {
|
||||
lines.push(` - ${agentName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,4 @@ export type {
|
||||
ResumeState,
|
||||
VulnExploitPipelineResult,
|
||||
} from './shared.js';
|
||||
export { PipelineExecutionError } from './shared.js';
|
||||
export { pentestPipeline } from './workflows.js';
|
||||
|
||||
@@ -60,21 +60,6 @@ export interface PipelineState {
|
||||
summary: PipelineSummary | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown by pentestPipeline() when the run fails, carrying the fully-populated
|
||||
* PipelineState (real agentMetrics, completedAgents, summary) so a consumer can
|
||||
* report actual spend instead of synthesizing a zeroed failed state. `cause`
|
||||
* preserves the original error for classification and Temporal failure reporting.
|
||||
*/
|
||||
export class PipelineExecutionError extends Error {
|
||||
override name = 'PipelineExecutionError' as const;
|
||||
readonly state: PipelineState;
|
||||
constructor(message: string, state: PipelineState, options?: { cause?: unknown }) {
|
||||
super(message, options);
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
// Extended state returned by getProgress query (includes computed fields)
|
||||
export interface PipelineProgress extends PipelineState {
|
||||
workflowId: string;
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { ActivityInput } from './activities.js';
|
||||
import {
|
||||
type AgentMetrics,
|
||||
getProgress,
|
||||
PipelineExecutionError,
|
||||
type PipelineInput,
|
||||
type PipelineProgress,
|
||||
type PipelineState,
|
||||
@@ -718,9 +717,10 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
});
|
||||
}
|
||||
|
||||
// Carry the populated state so a consumer can report real spend instead of a zeroed
|
||||
// failed state. The original error rides as `cause` for classification/reporting.
|
||||
throw new PipelineExecutionError(state.error ?? 'Pipeline failed', state, { cause: error });
|
||||
// Terminate the workflow in Temporal's FAILED state. WARNING: this must be an
|
||||
// ApplicationFailure — any other thrown type becomes an unhandled workflow-task failure
|
||||
// that Temporal retries indefinitely, leaving the run stuck in RUNNING.
|
||||
throw ApplicationFailure.nonRetryable(state.error ?? 'Pipeline failed', 'PipelineExecutionError');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user