diff --git a/internal/ingestion/component/knowledge_compiler/common/common_test.go b/internal/ingestion/component/knowledge_compiler/common/common_test.go
index 958850594a..5717735a67 100644
--- a/internal/ingestion/component/knowledge_compiler/common/common_test.go
+++ b/internal/ingestion/component/knowledge_compiler/common/common_test.go
@@ -15,7 +15,7 @@ func TestRetryFailureReporterCoalescesRepeatedErrors(t *testing.T) {
reporter := RetryFailureReporter{}
err := errors.New("429 rate limit")
- if message, ok := reporter.FailureMessage(1, 6, time.Second, err, false); !ok || !strings.Contains(message, "attempt 1/6") {
+ if message, ok := reporter.FailureMessage(1, 6, time.Second, err, false); !ok || !strings.HasPrefix(message, "[WARN] LLM call failed (attempt 1/6)") {
t.Fatalf("first failure = (%q, %v), want an emitted attempt message", message, ok)
}
if message, ok := reporter.FailureMessage(2, 6, 2*time.Second, err, false); ok || message != "" {
@@ -24,7 +24,7 @@ func TestRetryFailureReporterCoalescesRepeatedErrors(t *testing.T) {
if message, ok := reporter.FailureMessage(3, 6, 4*time.Second, err, false); ok || message != "" {
t.Fatalf("third repeated failure = (%q, %v), want suppressed", message, ok)
}
- if message, ok := reporter.FailureMessage(4, 6, 8*time.Second, err, false); !ok || !strings.Contains(message, "3 repeated failures") {
+ if message, ok := reporter.FailureMessage(4, 6, 8*time.Second, err, false); !ok || !strings.HasPrefix(message, "[WARN] LLM call failed: 3 repeated failures") {
t.Fatalf("fourth failure = (%q, %v), want a coalesced retry update", message, ok)
}
if message, ok := reporter.RecoveryMessage(5); !ok || !strings.Contains(message, "recovered after 4 failures") {
@@ -94,6 +94,10 @@ func TestGenJSONReportsLLMFailure(t *testing.T) {
// budget. Without the override GenJSON sleeps ~62s per failed call.
func TestGenJSONRetryDelayOverride(t *testing.T) {
ctx := WithRetryDelay(context.Background(), time.Millisecond)
+ var messages []string
+ ctx = runtime.WithProgressMessageCallback(ctx, func(_, message string) {
+ messages = append(messages, message)
+ })
start := time.Now()
_, err := GenJSON(ctx, failingChat{err: errors.New("llm down")}, ChatRequest{})
if err == nil {
@@ -102,6 +106,9 @@ func TestGenJSONRetryDelayOverride(t *testing.T) {
if time.Since(start) > 5*time.Second {
t.Fatalf("override did not shorten retry backoff: %s", time.Since(start))
}
+ if len(messages) != 3 || !strings.HasPrefix(messages[0], "[WARN]") || !strings.HasPrefix(messages[1], "[WARN]") || !strings.HasPrefix(messages[2], "[ERROR] LLM call failed after 6 attempts") {
+ t.Fatalf("unexpected retry progress messages: %v", messages)
+ }
}
func TestCompactErrorRedactsCredentials(t *testing.T) {
diff --git a/internal/ingestion/component/knowledge_compiler/common/retry_progress.go b/internal/ingestion/component/knowledge_compiler/common/retry_progress.go
index bfa704252e..d4be6db0af 100644
--- a/internal/ingestion/component/knowledge_compiler/common/retry_progress.go
+++ b/internal/ingestion/component/knowledge_compiler/common/retry_progress.go
@@ -23,18 +23,22 @@ func (r *RetryFailureReporter) FailureMessage(attempt, total int, delay time.Dur
if r == nil {
return "", false
}
+ level := "[WARN]"
+ if final {
+ level = "[ERROR]"
+ }
compact := CompactError(err)
if compact != r.lastError {
r.lastError = compact
r.failures = 1
- return formatRetryFailure(attempt, total, delay, compact), true
+ return fmt.Sprintf("%s LLM call failed (attempt %d/%d): %s%s", level, attempt, total, compact, retryDelaySuffix(delay)), true
}
r.failures++
if final {
- return fmt.Sprintf("[ERROR] LLM call failed after %d attempts (%d repeated failures): %s", attempt, r.failures-1, compact), true
+ return fmt.Sprintf("%s LLM call failed after %d attempts (%d repeated failures): %s", level, attempt, r.failures-1, compact), true
}
if r.failures > 1 && (r.failures-1)%retryFailureReportEvery == 0 {
- return fmt.Sprintf("[ERROR] LLM call failed: %d repeated failures (latest attempt %d/%d): %s%s", r.failures-1, attempt, total, compact, retryDelaySuffix(delay)), true
+ return fmt.Sprintf("%s LLM call failed: %d repeated failures (latest attempt %d/%d): %s%s", level, r.failures-1, attempt, total, compact, retryDelaySuffix(delay)), true
}
return "", false
}
@@ -51,10 +55,6 @@ func (r *RetryFailureReporter) RecoveryMessage(attempt int) (string, bool) {
return message, true
}
-func formatRetryFailure(attempt, total int, delay time.Duration, compact string) string {
- return fmt.Sprintf("[ERROR] LLM call failed (attempt %d/%d): %s%s", attempt, total, compact, retryDelaySuffix(delay))
-}
-
func retryDelaySuffix(delay time.Duration) string {
if delay <= 0 {
return ""
diff --git a/web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx b/web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
index 3218ef50f5..60a2a2fcc4 100644
--- a/web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
+++ b/web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
@@ -98,6 +98,8 @@ export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
className={cn('break-all', {
'text-state-error':
x.message.startsWith('[ERROR]'),
+ 'text-state-warning':
+ x.message.startsWith('[WARN]'),
})}
>
{x.message}
diff --git a/web/src/pages/dataset/compilation/progress-log-panel.tsx b/web/src/pages/dataset/compilation/progress-log-panel.tsx
index feb5ff2225..d812a88ec3 100644
--- a/web/src/pages/dataset/compilation/progress-log-panel.tsx
+++ b/web/src/pages/dataset/compilation/progress-log-panel.tsx
@@ -46,10 +46,20 @@ export function ProgressLogPanel({
-
+
{entry.text}
diff --git a/web/src/pages/dataset/compilation/utils/parse-progress-msg.ts b/web/src/pages/dataset/compilation/utils/parse-progress-msg.ts
index 8420bbe469..4cfacd13cf 100644
--- a/web/src/pages/dataset/compilation/utils/parse-progress-msg.ts
+++ b/web/src/pages/dataset/compilation/utils/parse-progress-msg.ts
@@ -2,6 +2,7 @@ export interface IProgressLogEntry {
key: number;
text: string;
isError: boolean;
+ isWarning: boolean;
}
export const parseProgressMsg = (progressMsg?: string): IProgressLogEntry[] =>
@@ -12,4 +13,5 @@ export const parseProgressMsg = (progressMsg?: string): IProgressLogEntry[] =>
key: index,
text: line,
isError: line.includes('[ERROR]') || line.startsWith('ERROR:'),
+ isWarning: line.includes('[WARN]'),
}));
diff --git a/web/src/pages/dataset/process-log-modal.test.tsx b/web/src/pages/dataset/process-log-modal.test.tsx
index c93dcc03e9..9d6bf9654d 100644
--- a/web/src/pages/dataset/process-log-modal.test.tsx
+++ b/web/src/pages/dataset/process-log-modal.test.tsx
@@ -63,14 +63,27 @@ describe('ProcessLogModal ingestion events', () => {
event_type: 1,
component: '',
phase: 0,
- message: 'embedding 40/120 chunks',
+ message: '[WARN] LLM call failed; retrying in 2s',
+ },
+ {
+ id: 3,
+ ts: '2026-09-18T10:21:10+08:00',
+ event_type: 2,
+ component: 'Compiler',
+ phase: 0,
+ message: '[ERROR] LLM call failed after 6 attempts',
},
]);
const times = screen.getAllByTestId('ingestion-event-time');
- expect(times).toHaveLength(2);
+ expect(times).toHaveLength(3);
expect(times[0].textContent).toBe(formatTime(eventTs));
- expect(screen.getByText('embedding 40/120 chunks')).toBeInTheDocument();
+ expect(
+ screen.getByText('[WARN] LLM call failed; retrying in 2s'),
+ ).toHaveClass('text-state-warning');
+ expect(
+ screen.getByText('[ERROR] LLM call failed after 6 attempts'),
+ ).toHaveClass('text-state-error');
});
it('omits the timestamp span when ts is absent', () => {
diff --git a/web/src/pages/dataset/process-log-modal.tsx b/web/src/pages/dataset/process-log-modal.tsx
index c85ab46353..24bfb049f8 100644
--- a/web/src/pages/dataset/process-log-modal.tsx
+++ b/web/src/pages/dataset/process-log-modal.tsx
@@ -79,10 +79,17 @@ export const replaceText = (text: string) => {
const replacedText = reactStringReplace(
nextText,
- /(\[ERROR\].+\s)/g,
+ /(\[(?:ERROR|WARN)\][^\r\n]*)/g,
(match, i) => {
return (
-
+
{match}
);