Skip to content

Feat/error handling recovery - #593

Merged
ginccc merged 10 commits into
mainfrom
feat/error-handling-recovery
Jul 20, 2026
Merged

Feat/error handling recovery#593
ginccc merged 10 commits into
mainfrom
feat/error-handling-recovery

Conversation

@ginccc

@ginccc ginccc commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

This pull request delivers a comprehensive overhaul of error handling and recovery infrastructure across the EDDI platform. The changes introduce a shared, configurable retry mechanism, robust error classification and reporting, enhanced admin recovery tools, and improved monitoring hooks. These updates span multiple subsystems (LLM, HTTP, MCP), enabling more resilient and observable operations, and providing administrators with better tools to recover from failures.

Error Handling & Retry Infrastructure

  • Introduced a reusable RetryConfiguration class for consistent, exponential-backoff retry logic across all subsystems (LLM, MCP, etc.), including static utility methods for executing actions with retries and detecting retryable errors.
  • Added per-call retry and error resilience options to the McpCall model, including continueOnError and a configurable retry field. This allows MCP calls to be retried on transient failures and to optionally not abort the pipeline on error. [1] [2] [3]

Admin Recovery & State Management

  • Added a new admin-only REST endpoint PATCH /{conversationId}/state to reset the state of stuck conversations (ERROR or EXECUTION_INTERRUPTED) back to READY, with appropriate validation and error handling. [1] [2] [3] [4] [5]
  • Extended the IConversationService interface and implementation to support atomic conversation state resets, affecting both persistent and in-memory state. [1] [2]

Monitoring & Observability

  • Enhanced event and streaming interfaces (ConversationEventSink, streaming handlers) to include structured onTaskFailed callbacks, enabling real-time admin monitoring of failed tasks with detailed error metadata. [1] [2] [3] [4]

Documentation

  • Updated the changelog with a detailed summary of the new error handling and recovery capabilities, design decisions, and rationale for the changes.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • ♻️ Refactoring (no functional changes)
  • 🔧 Chore (dependency updates, CI changes, etc.)

Checklist

  • My code follows the project's code style
  • I have added tests that prove my fix/feature works
  • Existing tests pass locally (./mvnw clean verify -DskipITs)
  • I have updated documentation if needed
  • My commit messages follow conventional commits
  • I have not committed any secrets, API keys, or tokens
  • This PR has a clear, focused scope (one concern per PR)

Summary by CodeRabbit

  • New Features
    • Shared configurable exponential-backoff retry used across tool calls, LLM calls, and streaming (including retries for zero-token failures while preserving partial results).
    • Policy-driven LLM response validation (empty/truncation/content-filter/refusal/streaming-timeout) with configurable fallback and warnings.
    • Admin REST endpoint to reset stuck conversations to READY, plus new SSE task_failed events with errorType and a safe, truncated errorSummary.
    • MCP tool discovery with per-server circuit breaking and MCP calls supporting continueOnError and per-call retry settings.
  • Bug Fixes
    • Recovery from interrupted conversations (resets to READY).
    • More reliable streaming retry/timeout handling (no stale timeout metadata; non-positive max-attempts execute once) and improved partial-timeout/partial-error behavior.
    • Safer error classification/summaries for metrics and audits, plus improved HTTP error-body capture and non-JSON response handling.

ginccc added 2 commits July 7, 2026 17:30
…ture

- Shared RetryConfiguration with exponential backoff and retryable error classification
- LifecycleManager: error classification, failure audit, SSE task_failed events, Micrometer counters
- Admin PATCH endpoint to reset stuck conversations (DB + cache atomic update)
- HTTP: error body storage for 4xx/5xx, softened JSON parsing
- MCP: continueOnError, call-site retry, circuit breaker (3 failures/60s cooldown)
- LLM: ResponseValidation config (onEmpty/onTruncation/onContentFilter/onRefusal/onStreamingTimeout)
- Streaming: retry on zero-token failures, partial response metadata, configurable timeout
- Conversation auto-recovery from EXECUTION_INTERRUPTED state
- Code review fixes: JSON injection, cache invalidation, tightened string matching, LifecycleException
- 83+ new tests across 7 test files (all passing)
Prevents checked exceptions from runPostResponse() from masking the
original LifecycleException or defeating continueOnError when post-response
processing fails during MCP call error handling.
@ginccc
ginccc requested a review from rolandpickl as a code owner July 8, 2026 14:39
@ginccc
ginccc requested a review from Copilot July 8, 2026 14:39
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds shared retry and recovery infrastructure across LLM, MCP, API, streaming, lifecycle, and conversation-state flows. It adds response validation, circuit breaking, structured task-failure reporting, HTTP error persistence, an admin reset endpoint, tests, and changelog entries.

Changes

Error Handling and Recovery Infrastructure

Layer / File(s) Summary
Shared retry contracts and MCP resilience
src/main/java/ai/labs/eddi/configs/..., src/main/java/ai/labs/eddi/modules/mcpcalls/..., src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java, src/test/java/ai/labs/eddi/configs/..., src/test/java/ai/labs/eddi/modules/llm/impl/...
Adds shared retry/backoff behavior, MCP retry and continue-on-error handling, per-server circuit breaking, and supporting tests.
LLM execution, streaming recovery, and validation
src/main/java/ai/labs/eddi/modules/llm/..., src/test/java/ai/labs/eddi/modules/llm/...
Adds task-level response-validation policies, finish-reason warnings, streaming retries and timeouts, metadata isolation, and validation actions.
API, lifecycle, and task-failure reporting
src/main/java/ai/labs/eddi/modules/apicalls/..., src/main/java/ai/labs/eddi/engine/lifecycle/..., src/main/java/ai/labs/eddi/engine/api/..., src/main/java/ai/labs/eddi/engine/internal/..., src/test/java/ai/labs/eddi/engine/...
Persists HTTP error responses, softens invalid JSON parsing, classifies failures, records metrics and audit data, and emits task_failed SSE events.
Conversation recovery
src/main/java/ai/labs/eddi/engine/api/..., src/main/java/ai/labs/eddi/engine/internal/..., src/main/java/ai/labs/eddi/engine/runtime/internal/..., src/test/java/ai/labs/eddi/engine/...
Adds an admin state-reset endpoint and automatic recovery for interrupted conversations.
Changelog and verification updates
docs/changelog.md, src/test/java/...
Documents the retry and recovery changes and updates regression coverage for the resulting contracts.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LifecycleManager
  participant ConversationService
  participant RestAgentEngineStreaming
  participant Client
  LifecycleManager->>ConversationService: onTaskFailed(taskId, taskType, durationMs, errorType, errorSummary)
  ConversationService->>RestAgentEngineStreaming: streamingHandler.onTaskFailed(...)
  RestAgentEngineStreaming->>Client: task_failed SSE event
Loading
sequenceDiagram
  participant LlmTask
  participant StreamingLegacyChatExecutor
  participant StreamingModel
  LlmTask->>StreamingLegacyChatExecutor: execute(model, messages, eventSink, task)
  StreamingLegacyChatExecutor->>StreamingModel: stream request
  StreamingModel-->>StreamingLegacyChatExecutor: partial response, completion, or error
  StreamingLegacyChatExecutor-->>LlmTask: StreamingResult(response, metadata)
  LlmTask->>LlmTask: applyResponseValidation(response, metadata, task)
Loading
sequenceDiagram
  participant Admin
  participant RestAgentEngine
  participant IConversationMemoryStore
  participant ConversationService
  Admin->>RestAgentEngine: PATCH /{conversationId}/state?state=READY
  RestAgentEngine->>IConversationMemoryStore: load conversation snapshot
  RestAgentEngine->>ConversationService: resetConversationState(conversationId, READY)
  ConversationService-->>Admin: HTTP response
Loading

Possibly related PRs

  • labsai/EDDI#424: Both modify LifecycleManager task-failure handling and observability.
  • labsai/EDDI#505: Both modify structured task-failure callback identifiers and SSE plumbing.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the PR, but it's too vague to convey the main change. Rename it to describe the primary change, e.g. "Add shared retry and conversation recovery handling".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/error-handling-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java Fixed
// Auto-recover from transient interrupted state
if (getConversationState() == ConversationState.EXECUTION_INTERRUPTED) {
LOGGER.infof("Auto-recovering conversation %s from EXECUTION_INTERRUPTED",
conversationMemory.getConversationId());

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens EDDI’s resilience and observability by introducing shared retry utilities, richer error classification/reporting, MCP/LLM recovery behaviors, and an admin capability to recover stuck conversations—aimed at making pipeline execution more robust across LLM, HTTP, and MCP subsystems.

Changes:

  • Added a shared RetryConfiguration (and integrated it into LLM + MCP execution paths) plus expanded error classification/audit/SSE reporting in LifecycleManager.
  • Introduced LLM response validation policies and improved streaming execution metadata/timeout handling.
  • Added an admin-only REST endpoint to reset stuck conversation state, and extended streaming/event interfaces to emit structured task-failure events.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java New shared retry/backoff utility used across subsystems.
src/test/java/ai/labs/eddi/configs/shared/RetryConfigurationTest.java Tests for shared retry/backoff behavior.
src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java Adds response validation + streaming timeout fields; extracts retry config to shared model while keeping compat wrapper.
src/test/java/ai/labs/eddi/modules/llm/model/ResponseValidationTest.java Tests defaulting/serialization for new response-validation configuration.
src/main/java/ai/labs/eddi/modules/llm/impl/AgentExecutionHelper.java Switches LLM retry implementation to shared RetryConfiguration.
src/main/java/ai/labs/eddi/modules/llm/impl/LegacyChatExecutor.java Null-safety for AiMessage; adds finish-reason warnings into response metadata.
src/test/java/ai/labs/eddi/modules/llm/impl/LegacyChatExecutorErrorHandlingTest.java Tests finish-reason warnings and null-safety.
src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java Adds task-aware streaming execution with timeout/retry and metadata capture.
src/test/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutorRetryTest.java Tests retry/timeout/metadata behavior for streaming legacy executor.
src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java Applies response validation policies after model execution; ingests streaming metadata.
src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java Adds circuit breaker tracking to reduce repeated MCP discovery failures.
src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerCircuitBreakerTest.java Tests circuit breaker behavior for MCP tool discovery.
src/main/java/ai/labs/eddi/configs/mcpcalls/model/McpCall.java Adds per-call continueOnError and optional retry config.
src/test/java/ai/labs/eddi/configs/mcpcalls/model/McpCallsModelsTest.java Tests new MCP call fields and JSON round-trips.
src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java Adds per-call retry and continueOnError handling for MCP tool execution.
src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutor.java Stores non-2xx error body/code in memory; softens JSON parsing when content-type is JSON but body isn’t.
src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java Extends sink with onTaskFailed callback for structured failure monitoring.
src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java Adds error classification + audit summary + SSE task-failure emission + metrics tags.
src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerErrorClassificationTest.java Tests classification + audit summary behavior.
src/main/java/ai/labs/eddi/engine/internal/ConversationService.java Passes through new onTaskFailed streaming callback; adds resetConversationState.
src/main/java/ai/labs/eddi/engine/api/IConversationService.java Adds resetConversationState API + default streaming callback.
src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java Adds admin-only PATCH /{conversationId}/state API contract.
src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java Implements admin state reset endpoint and wires in conversation memory snapshot loading.
src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java Updates constructor wiring for new RestAgentEngine dependency.
src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java Emits task_failed SSE events with structured error metadata.
src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java Adds automatic recovery from EXECUTION_INTERRUPTED to READY at step start.
docs/changelog.md Documents the error-handling/recovery overhaul and design decisions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java
Comment thread src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
Comment thread src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutor.java (1)

169-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider appending a truncation indicator to the error body.

When the error body exceeds 2000 chars, the truncated string is stored without any marker. Downstream templates or rules inspecting the error body cannot distinguish a truncated response from a complete one. Appending a simple suffix (e.g., "…[truncated]") would make truncation visible to consumers.

This is a minor usability improvement and can be deferred if not a priority.

♻️ Optional: add truncation indicator
-                                if (errorBody.length() > 2000
-                                        ? errorBody.substring(0, 2000)
-                                        : errorBody;
+                                String truncatedError = errorBody.length() > 2000
+                                        ? errorBody.substring(0, 2000) + "…[truncated]"
+                                        : errorBody;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutor.java` around
lines 169 - 175, The fallback path in ApiCallExecutor around responseBody
handling currently stores a truncated error body without any visible marker, so
consumers can’t tell it was shortened. Update the truncation logic in this
JSON-deserialization fallback flow to append a clear suffix like “…[truncated]”
whenever the body is cut off, and keep the behavior localized to the
responseObject assignment used after the deserialize attempt.
src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java (1)

20-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Configuration POJO should use Java records per coding guidelines.

RetryConfiguration.java matches the **/*Configuration.java pattern, and the coding guidelines state: "New feature configuration classes must use Java records for configuration POJOs." The current mutable class with getters/setters violates this guideline.

Consider splitting into a RetryConfig record for the configuration fields and a separate utility class (e.g., RetryExecutor) for the static methods (executeWithRetry, backoff, isRetryableError). Jackson deserialization of records is fully supported via the canonical constructor.

As per coding guidelines: "New feature configuration classes must use Java records for configuration POJOs."

♻️ Proposed refactor: split configuration and utility
// RetryConfig.java — configuration record
public record RetryConfig(
    Integer maxAttempts,
    Long backoffDelayMs,
    Double backoffMultiplier,
    Long maxBackoffDelayMs
) {
    public RetryConfig {
        if (maxAttempts == null) maxAttempts = 3;
        if (backoffDelayMs == null) backoffDelayMs = 1000L;
        if (backoffMultiplier == null) backoffMultiplier = 2.0;
        if (maxBackoffDelayMs == null) maxBackoffDelayMs = 10000L;
    }
}

// RetryExecutor.java — static utility methods
public final class RetryExecutor {
    public static <T> T executeWithRetry(Callable<T> action, RetryConfig config, String desc) { ... }
    public static void backoff(int attempt, RetryConfig config) { ... }
    public static boolean isRetryableError(Exception e) { ... }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java` around
lines 20 - 58, The RetryConfiguration POJO should be converted to a Java record
to match the configuration coding guideline, since the current mutable class
with getters/setters violates the record requirement. Replace the mutable
configuration fields in RetryConfiguration with an immutable record form (or
rename to RetryConfig) and keep only the config data there, while moving the
retry logic methods such as executeWithRetry, backoff, and isRetryableError into
a separate utility class like RetryExecutor. Make sure any existing uses of
RetryConfiguration are updated to the new record type and that default values
remain preserved through the record canonical constructor.

Source: Coding guidelines

src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java (1)

570-667: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New Response Validation feature has no Micrometer metrics.

The validation actions (warn/fallback/error per signal) are a good candidate for counters (e.g., validation triggers by type/action), useful for tracking how often responses are being modified or rejected in production.

As per path instructions, "Use Micrometer MeterRegistry metrics for new features, initializing counters/timers in @PostConstruct."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java` around lines 570 -
667, The new response validation flow in
applyResponseValidation/applyValidationAction has no Micrometer instrumentation
yet. Add MeterRegistry-based counters for each validation signal/action pair
(for example, empty/truncated/content_filter/streaming_timeout/refusal and
warn/fallback/error) so triggers are tracked in production. Initialize the
meters in a `@PostConstruct` method on LlmTask, wire in MeterRegistry via the
class constructor or injection, and increment the appropriate counter inside
applyValidationAction before storing data, returning fallback content, or
throwing LifecycleException.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java`:
- Around line 90-96: The retry flow in StreamingLegacyChatExecutor is blocking
the caller by waiting on CountDownLatch and sleeping in
RetryConfiguration.backoff across multiple attempts. Refactor the attempt loop
and related timeout/error handling in the streaming execution path to run
asynchronously so the calling thread is not held for long periods; use the
existing StreamingLegacyChatExecutor methods and retry/backoff logic to complete
work off-thread and signal results without blocking, and update the
retry-related blocks around the latch await and backoff calls accordingly.
- Around line 95-186: The retry loop in StreamingLegacyChatExecutor leaves
timed-out or errored streaming attempts alive, so stale tokens can keep reaching
eventSink during the next attempt. Update the streaming flow around
streamingModel.chat and the StreamingChatResponseHandler callbacks to use a
cancelable context/handle or an active-attempt guard, and ensure the previous
attempt is cancelled or ignored before any continue to the next retry.

In `@src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java`:
- Around line 671-759: The new ResponseValidation configuration in
LlmConfiguration should be converted from a mutable bean into a Java record to
match the configuration POJO guideline. Update the ResponseValidation type
itself (including its default values and accessors), then adjust any Jackson
binding or deserialization assumptions and update ResponseValidationTest to
construct and verify the record instead of using setters. Keep the existing
field names and semantics so callers can still configure enabled, onEmpty,
onTruncation, onContentFilter, onRefusal, and onStreamingTimeout.

In
`@src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerCircuitBreakerTest.java`:
- Around line 122-138: Strengthen the circuit-breaker test in
circuitClosed_attemptsConnection by asserting a recorded failure after
discoverTools(List.of(config)) runs, not just a non-null result. Use the
McpToolProviderManager instance to verify the circuit state or failure count
changed as expected so the test proves the connection attempt actually happened
instead of being skipped by an always-open breaker.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java`:
- Around line 20-58: The RetryConfiguration POJO should be converted to a Java
record to match the configuration coding guideline, since the current mutable
class with getters/setters violates the record requirement. Replace the mutable
configuration fields in RetryConfiguration with an immutable record form (or
rename to RetryConfig) and keep only the config data there, while moving the
retry logic methods such as executeWithRetry, backoff, and isRetryableError into
a separate utility class like RetryExecutor. Make sure any existing uses of
RetryConfiguration are updated to the new record type and that default values
remain preserved through the record canonical constructor.

In `@src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutor.java`:
- Around line 169-175: The fallback path in ApiCallExecutor around responseBody
handling currently stores a truncated error body without any visible marker, so
consumers can’t tell it was shortened. Update the truncation logic in this
JSON-deserialization fallback flow to append a clear suffix like “…[truncated]”
whenever the body is cut off, and keep the behavior localized to the
responseObject assignment used after the deserialize attempt.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java`:
- Around line 570-667: The new response validation flow in
applyResponseValidation/applyValidationAction has no Micrometer instrumentation
yet. Add MeterRegistry-based counters for each validation signal/action pair
(for example, empty/truncated/content_filter/streaming_timeout/refusal and
warn/fallback/error) so triggers are tracked in production. Initialize the
meters in a `@PostConstruct` method on LlmTask, wire in MeterRegistry via the
class constructor or injection, and increment the appropriate counter inside
applyValidationAction before storing data, returning fallback content, or
throwing LifecycleException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c600bc68-357b-4451-8fd5-1dc75f137356

📥 Commits

Reviewing files that changed from the base of the PR and between dcd3e54 and c054b43.

📒 Files selected for processing (27)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/mcpcalls/model/McpCall.java
  • src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/ConversationEventSink.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutor.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/AgentExecutionHelper.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LegacyChatExecutor.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManager.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/main/java/ai/labs/eddi/modules/mcpcalls/impl/McpCallsTask.java
  • src/test/java/ai/labs/eddi/configs/mcpcalls/model/McpCallsModelsTest.java
  • src/test/java/ai/labs/eddi/configs/shared/RetryConfigurationTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerErrorClassificationTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/LegacyChatExecutorErrorHandlingTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/McpToolProviderManagerCircuitBreakerTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutorRetryTest.java
  • src/test/java/ai/labs/eddi/modules/llm/model/ResponseValidationTest.java

Comment thread src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java Outdated
Comment thread src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
ginccc added 2 commits July 16, 2026 00:33
…ration

The nested LlmConfiguration.RetryConfiguration was an empty subclass of the
extracted ai.labs.eddi.configs.shared.RetryConfiguration, added as a
backward-compat shim. It overrode nothing and shadowed the imported shared
type within LlmConfiguration's body, triggering the "class has same name as
super class" code-quality finding on PR #593.

Delete the shim and point the retry field/getter/setter plus all tests at the
shared class directly. The subclass added no fields, so the retry JSON
structure is byte-for-byte identical and existing stored configs deserialize
unchanged. Verified with a clean test-compile and 123 passing unit tests.
Integrates 180 commits from main — the HITL (human-in-the-loop) framework,
multimodal-attachments completion, and related work — into the holistic
error-handling/recovery branch.

Conflicts resolved as unions preserving BOTH feature sets:
- IConversationService / IRestAgentEngine: kept error-handling's
  resetConversationState + PATCH /state endpoint alongside main's HITL
  endConversation(id, endedBy), cancel/resume/approval endpoints, and onSkipped.
- AgentExecutionHelper: kept HEAD's one-liner delegating to the shared
  RetryConfiguration.executeWithRetry; main's inline retry loop (which
  referenced the now-removed nested RetryConfiguration and duplicated the
  shared util) was dropped. Its HITL ToolApprovalRequiredException pass-through
  was relocated into the shared RetryConfiguration so both LLM and MCP retries
  still let a tool-approval pause propagate intact.
- LlmConfiguration / LlmTask: unioned response-validation (HEAD) with
  multimodal + tool-approval config and the HITL resume path (main).
- RestAgentEngine constructor: unioned to take conversationMemoryStore (HEAD)
  plus hitlAccessGuard/hitlToolJournalStore (main); updated all affected tests.
- Five of main's test files updated for the removed nested RetryConfiguration
  and the new constructor arity.

Verified: clean compile (main + tests); 437 affected unit tests pass;
adversarial multi-agent review of all 8 conflict resolutions plus the 3 silent
auto-merges (Conversation, ConversationService, LifecycleManager) found no
dropped or corrupted code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java (4)

379-380: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log SSE callback failures with conversation context.

The current message drops the throwable, conversation ID, and task ID, making callback failures difficult to diagnose.

As per coding guidelines, JBoss Logger entries must include conversation context and use appropriate log levels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 379 - 380, Update the SSE callback failure handler around the catch
block in LifecycleManager to log the full throwable rather than only its
message, and include the conversation ID and task ID in the structured log
context. Use the appropriate warning/error level for a callback failure while
preserving the existing exception handling flow.

Source: Coding guidelines


384-400: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not let audit collection bypass strict-write recovery.

If auditCollector.collect() throws, Lines 403-408 never run, partial task writes remain, and the audit exception masks the original failure. Apply strict-write recovery first, then shield audit collection while attaching any reporting failure as suppressed.

As per coding guidelines, recoverable exceptions must not kill the pipeline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 384 - 400, The failure path must execute strict-write recovery
before attempting audit reporting. In the block around the failure AuditEntry
and auditCollector.collect, move or invoke the existing recovery logic first,
then wrap auditCollector.collect in guarded recoverable-exception handling;
attach any audit exception as suppressed to the original task failure without
replacing it, and ensure the pipeline continues through the existing recovery
flow.

Source: Coding guidelines


834-840: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Redact exception messages before persisting or streaming them.

Truncation does not make arbitrary exception text safe. This value is written to audit storage and emitted through task_failed SSE, potentially exposing URLs, payloads, credentials, or PII. Apply a shared redactor before returning it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 834 - 840, Update summarizeForAudit(Throwable e) to pass the
selected exception message or class-name fallback through the project’s shared
redactor before truncation and return. Preserve the existing 500-character
limit, and ensure the redacted value is used for both audit persistence and
task_failed SSE output.

802-823: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give typed inner causes precedence over wrapper messages.

Each outer message is checked before its cause. For example, a "429" wrapper around SocketTimeoutException is classified as rate_limit, not timeout. Scan the entire chain for typed causes before applying message heuristics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 802 - 823, Update classifyError to scan the full Throwable cause
chain for typed timeout or transport exceptions before evaluating any
error-message heuristics. Preserve the existing message classifications for
rate_limit and content_filter, but only apply them after no higher-priority
typed cause is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`:
- Around line 534-538: Update getLifecycleStartIndex to guard both nullable
values before prefix matching: skip lifecycle tasks whose task.getType() is null
and ignore null entries in lifecycleTaskTypes. Preserve the existing
startsWith-based matching and return the first matching task index.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`:
- Around line 379-380: Update the SSE callback failure handler around the catch
block in LifecycleManager to log the full throwable rather than only its
message, and include the conversation ID and task ID in the structured log
context. Use the appropriate warning/error level for a callback failure while
preserving the existing exception handling flow.
- Around line 384-400: The failure path must execute strict-write recovery
before attempting audit reporting. In the block around the failure AuditEntry
and auditCollector.collect, move or invoke the existing recovery logic first,
then wrap auditCollector.collect in guarded recoverable-exception handling;
attach any audit exception as suppressed to the original task failure without
replacing it, and ensure the pipeline continues through the existing recovery
flow.
- Around line 834-840: Update summarizeForAudit(Throwable e) to pass the
selected exception message or class-name fallback through the project’s shared
redactor before truncation and return. Preserve the existing 500-character
limit, and ensure the redacted value is used for both audit persistence and
task_failed SSE output.
- Around line 802-823: Update classifyError to scan the full Throwable cause
chain for typed timeout or transport exceptions before evaluating any
error-message heuristics. Preserve the existing message classifications for
rate_limit and content_filter, but only apply them after no higher-priority
typed cause is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eaa7c192-5fbf-473e-9247-344608980c19

📥 Commits

Reviewing files that changed from the base of the PR and between cb8e370 and 78b9de4.

📒 Files selected for processing (14)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorToolPauseTest.java
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java (4)

379-380: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log SSE callback failures with conversation context.

The current message drops the throwable, conversation ID, and task ID, making callback failures difficult to diagnose.

As per coding guidelines, JBoss Logger entries must include conversation context and use appropriate log levels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 379 - 380, Update the SSE callback failure handler around the catch
block in LifecycleManager to log the full throwable rather than only its
message, and include the conversation ID and task ID in the structured log
context. Use the appropriate warning/error level for a callback failure while
preserving the existing exception handling flow.

Source: Coding guidelines


384-400: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not let audit collection bypass strict-write recovery.

If auditCollector.collect() throws, Lines 403-408 never run, partial task writes remain, and the audit exception masks the original failure. Apply strict-write recovery first, then shield audit collection while attaching any reporting failure as suppressed.

As per coding guidelines, recoverable exceptions must not kill the pipeline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 384 - 400, The failure path must execute strict-write recovery
before attempting audit reporting. In the block around the failure AuditEntry
and auditCollector.collect, move or invoke the existing recovery logic first,
then wrap auditCollector.collect in guarded recoverable-exception handling;
attach any audit exception as suppressed to the original task failure without
replacing it, and ensure the pipeline continues through the existing recovery
flow.

Source: Coding guidelines


834-840: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Redact exception messages before persisting or streaming them.

Truncation does not make arbitrary exception text safe. This value is written to audit storage and emitted through task_failed SSE, potentially exposing URLs, payloads, credentials, or PII. Apply a shared redactor before returning it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 834 - 840, Update summarizeForAudit(Throwable e) to pass the
selected exception message or class-name fallback through the project’s shared
redactor before truncation and return. Preserve the existing 500-character
limit, and ensure the redacted value is used for both audit persistence and
task_failed SSE output.

802-823: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give typed inner causes precedence over wrapper messages.

Each outer message is checked before its cause. For example, a "429" wrapper around SocketTimeoutException is classified as rate_limit, not timeout. Scan the entire chain for typed causes before applying message heuristics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 802 - 823, Update classifyError to scan the full Throwable cause
chain for typed timeout or transport exceptions before evaluating any
error-message heuristics. Preserve the existing message classifications for
rate_limit and content_filter, but only apply them after no higher-priority
typed cause is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`:
- Around line 534-538: Update getLifecycleStartIndex to guard both nullable
values before prefix matching: skip lifecycle tasks whose task.getType() is null
and ignore null entries in lifecycleTaskTypes. Preserve the existing
startsWith-based matching and return the first matching task index.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`:
- Around line 379-380: Update the SSE callback failure handler around the catch
block in LifecycleManager to log the full throwable rather than only its
message, and include the conversation ID and task ID in the structured log
context. Use the appropriate warning/error level for a callback failure while
preserving the existing exception handling flow.
- Around line 384-400: The failure path must execute strict-write recovery
before attempting audit reporting. In the block around the failure AuditEntry
and auditCollector.collect, move or invoke the existing recovery logic first,
then wrap auditCollector.collect in guarded recoverable-exception handling;
attach any audit exception as suppressed to the original task failure without
replacing it, and ensure the pipeline continues through the existing recovery
flow.
- Around line 834-840: Update summarizeForAudit(Throwable e) to pass the
selected exception message or class-name fallback through the project’s shared
redactor before truncation and return. Preserve the existing 500-character
limit, and ensure the redacted value is used for both audit persistence and
task_failed SSE output.
- Around line 802-823: Update classifyError to scan the full Throwable cause
chain for typed timeout or transport exceptions before evaluating any
error-message heuristics. Preserve the existing message classifications for
rate_limit and content_filter, but only apply them after no higher-priority
typed cause is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eaa7c192-5fbf-473e-9247-344608980c19

📥 Commits

Reviewing files that changed from the base of the PR and between cb8e370 and 78b9de4.

📒 Files selected for processing (14)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorToolPauseTest.java
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/Conversation.java
  • src/main/java/ai/labs/eddi/engine/api/IRestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/main/java/ai/labs/eddi/configs/shared/RetryConfiguration.java
🛑 Comments failed to post (1)
src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java (1)

534-538: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard nullable task types before prefix matching.

Elsewhere task.getType() is explicitly treated as nullable, but this dereference crashes selective execution. Null-filter requested types as well.

Proposed fix
 ILifecycleTask task = this.lifecycleTasks.get(i);
-if (lifecycleTaskTypes.stream().anyMatch(type -> task.getType().startsWith(type))) {
+String taskType = task.getType();
+if (taskType != null && lifecycleTaskTypes.stream()
+        .filter(Objects::nonNull)
+        .anyMatch(taskType::startsWith)) {
     return i;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    private int getLifecycleStartIndex(List<String> lifecycleTaskTypes) {
        for (int i = 0; i < this.lifecycleTasks.size(); i++) {
            ILifecycleTask task = this.lifecycleTasks.get(i);
            String taskType = task.getType();
            if (taskType != null && lifecycleTaskTypes.stream()
                    .filter(Objects::nonNull)
                    .anyMatch(taskType::startsWith)) {
                return i;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java`
around lines 534 - 538, Update getLifecycleStartIndex to guard both nullable
values before prefix matching: skip lifecycle tasks whose task.getType() is null
and ignore null entries in lifecycleTaskTypes. Preserve the existing
startsWith-based matching and return the first matching task index.

ginccc added 2 commits July 16, 2026 09:21
Both failures pre-date the origin/main merge — CI was already red on
c054b43 ("Tests run: 9776, Failures: 1, Errors: 1") with these exact two
tests. Each asserts a contract this PR deliberately superseded, so the fix
is in the tests; no production behavior changes.

StreamingLegacyChatExecutorTest.execute_error_throwsRuntimeException
asserted that a streaming error always throws. The PR intentionally changed
this: an error after partial tokens returns the partial text with a
streaming_error_partial warning, and only a zero-content error throws.
Retarget the test at the zero-content case (matching its name) and add
execute_errorAfterPartial_returnsPartialContent for the partial contract,
keeping the original token-forwarding assertion.

ConversationExtendedTest.saySucceeds stubbed getConversationState() with a
call-count-sensitive consecutive-return sequence (READY, then IN_PROGRESS).
The PR's EXECUTION_INTERRUPTED auto-recovery added a state read at the top
of runStep, which consumed the READY, so the in-progress guard saw
IN_PROGRESS and threw. The mock now tracks state like real memory, making it
robust to how often production reads it.
Four findings on this PR's own failure path, all verified against the code.
Each fix reuses existing infrastructure rather than adding a new utility.

Audit must not bypass strict-write recovery (Major): if auditCollector.collect()
threw, the strict-write rollback was skipped — leaving the partial task writes
it exists to remove — and the audit exception propagated out of the catch,
replacing the original task failure. Strict-write recovery now runs first, and
audit collection is shielded, attaching any reporting error to the original
exception via addSuppressed instead of masking it.

Redact credentials from audit/SSE summaries (Major): summarizeForAudit() only
truncated, yet its output is persisted to the audit ledger and streamed to
admins over task_failed SSE. It now applies SecretRedactionFilter.redact()
before truncating — cutting first can split a secret so the pattern no longer
matches. URLs and class names stay: the audience is privileged and needs them.

Typed causes outrank wrapper messages (Minor): classifyError() checked each
level's message before descending, so a "429" wrapper around a
SocketTimeoutException classified as rate_limit. It now scans the chain for
typed causes first, falling back to message heuristics only when none match.

SSE failure logging (Minor): the task_failed emission catch logged at DEBUG and
dropped the throwable and all context. Now WARN with the throwable, sanitized
conversation id (LogSanitizer, CWE-117) and task id.

Adds four regression tests, each failing under the previous behavior.
@ginccc
ginccc requested a review from aisabella-ai July 20, 2026 16:34
ginccc added 2 commits July 20, 2026 20:11
…conflicts

Brings 43 commits from main (group conversations, MCP/REST ownership
hardening, multi-model cascade enterprise work) into the error-handling
branch.

Conflicts resolved (7):
- RestAgentEngine + its three tests: main moved the descriptor-based
  ownership check into ConversationAccessGuard (removing the last
  conversationDescriptorStore usage); ours added IConversationMemoryStore
  for the new admin resetState() endpoint. Kept the memory store, dropped
  the descriptor store.
- CascadingModelExecutorCoverageTest: resolved onto the shared
  ai.labs.eddi.configs.shared.RetryConfiguration.
- StreamingLegacyChatExecutor: ours added a retry loop and configurable
  timeout, main added executeCapturing() capturing full ChatResponse
  metadata for cascade cost evidence. Unified on one core implementation
  that does both; both public entry points retained.
- docs/changelog.md: both entry blocks kept whole.

Beyond the reported conflicts:
- Repaired two files broken by a silent auto-merge: main added new
  LlmConfiguration.RetryConfiguration usages while ours deleted that shim,
  so the merge compiled on neither side (AgentOrchestratorExtendedTest,
  CascadingModelExecutorEnterpriseTest).
- executeCapturing now always propagates a mid-stream error instead of
  salvaging partial text. CascadingModelExecutor has no try/catch there and
  relies on the throw to fall back to the best previous step, so salvaging
  caused a failed final step to be accepted as successful.

Verified: clean test-compile green; full unit suite 11,409 tests with no
assertion failures (remaining failures/errors are all sandbox loopback-socket
and Docker/Testcontainers limitations, verified per-message).
…Executor

Both pre-date the merge — they came in with this branch's retry loop — but
both sit inside the method the merge rewrote and are squarely in this PR's
subject area, so they are fixed here rather than deferred.

1. A failed attempt's metadata leaked into a successful retry. The metadata
   map is declared outside the retry loop; when attempt 1 timed out with no
   tokens it set streamingTimeout=true, and a successful attempt 2 returned
   carrying that stale flag. LlmTask.applyResponseValidation acts on it and
   fires responseValidation.onStreamingTimeout, so a good answer was either
   silently replaced by the timeout fallback string or turned into a thrown
   turn. The loop now clears the map at the start of each attempt.

2. maxAttempts <= 0 skipped the model entirely. RetryConfiguration does no
   clamping, so retry: {maxAttempts: 0} — a natural way to write "no retries"
   — made the loop body never execute: the model was never invoked and a null
   response propagated into a TextOutputItem(null, 0) with no exception and
   no log. Clamped with Math.max(1, ...).

Not clamped in RetryConfiguration.setMaxAttempts itself: the shared
executeWithRetry already fails loudly at 0 (throws LifecycleException), and
changing the setter would silently alter that contract for the MCP and
HTTP-call consumers and their tests. The clamp belongs at the call site that
was failing silently.

Tests: three regression tests, each verified to fail before the fix and pass
after. A fourth pins executeCapturing's throw-on-partial-token contract at
the executor level, which until now was guarded only by a cascade-level test.
155 tests green across the streaming, cascade, orchestrator and LlmTask suites.

Also fixes an import-ordering slip introduced by the merge resolution in
AgentOrchestratorExtendedTest.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java (1)

176-230: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Interrupted streaming attempt is silently returned as a success.

When latch.await(...) throws InterruptedException (176-185), neither timedOut nor errorRef is set. Execution then falls through both the if (timedOut) and if (errorRef.get() != null) checks and reaches break → returns new StreamingResult(responseText, metadata) as if the attempt completed normally — even if responseText is empty and no ChatResponse was ever received. Callers (e.g. LlmTask, the cascade) have no signal that this "successful" result is actually the product of an interrupted/aborted attempt.

🐛 Proposed fix — treat interruption as a distinct failure signal
             boolean timedOut = false;
+            boolean interrupted = false;
             try {
                 if (!latch.await(timeoutSeconds, TimeUnit.SECONDS)) {
                     LOGGER.warn("Streaming chat timed out");
                     timedOut = true;
                 }
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
                 LOGGER.warn("Streaming chat was interrupted");
+                interrupted = true;
             }
 
             responseText = fullResponse.toString();
             metadata.putAll(buildMetadata(responseRef.get()));
 
+            if (interrupted) {
+                metadata.put("streamingInterrupted", true);
+                return new StreamingResult(responseText, metadata);
+            }
+
             if (timedOut) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java`
around lines 176 - 230, Update the interruption handling around latch.await in
the streaming retry flow to record interruption as a distinct failure outcome
instead of falling through to the success break. Preserve the interrupt flag,
propagate or retry the interruption consistently with other failed attempts, and
ensure an interrupted empty attempt cannot return a successful StreamingResult.
Anchor the change to the timedOut/errorRef checks and the retry loop in
StreamingLegacyChatExecutor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changelog.md`:
- Line 90: Reconcile the full-suite summary in the changelog entry by rechecking
the Surefire aggregation and correcting the reported failures, errors, and
category counts so the totals are mathematically consistent. Update the
surrounding test-count statement if necessary, while preserving the explanation
that these are sandbox infrastructure issues rather than assertion failures.

In
`@src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerTest.java`:
- Line 438: In LifecycleManagerTest, add a top-level import for
IAuditEntryCollector and replace the fully qualified type in the auditCollector
mock declaration with the simple class name.

---

Outside diff comments:
In
`@src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java`:
- Around line 176-230: Update the interruption handling around latch.await in
the streaming retry flow to record interruption as a distinct failure outcome
instead of falling through to the success break. Preserve the interrupt flag,
propagate or retry the interruption consistently with other failed attempts, and
ensure an interrupted empty attempt cannot return a successful StreamingResult.
Anchor the change to the timedOut/errorRef checks and the retry loop in
StreamingLegacyChatExecutor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f0df1a5-6050-4a39-b276-2ca07dc50d05

📥 Commits

Reviewing files that changed from the base of the PR and between 33d85cd and 746982d.

📒 Files selected for processing (21)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutor.java
  • src/main/java/ai/labs/eddi/modules/llm/model/LlmConfiguration.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java
  • src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerErrorClassificationTest.java
  • src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/AgentOrchestratorExtendedTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorCoverageTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorEnterpriseTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorExecuteTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorTest.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/LlmTaskCoverage2Test.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/StreamingLegacyChatExecutorRetryTest.java
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorTest.java
  • src/main/java/ai/labs/eddi/engine/api/IConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngineStreaming.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java
  • src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
  • src/test/java/ai/labs/eddi/modules/llm/impl/CascadingModelExecutorExecuteTest.java
  • src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManager.java

Comment thread docs/changelog.md Outdated

`mvnw clean test-compile` green — a *clean* build deliberately, since the `RestAgentEngine` signature change would be masked by a stale incremental one.

Full unit suite: **11,409 tests run, no assertion failures.** The 8 reported failures and 304 errors were each inspected via the surefire XML and categorised by message: 297 `Unable to establish loopback connection`, 16 Docker/Testcontainers unavailable (Mongo + Postgres store tests), 5 socket-selector/event-loop creation errors. All are this sandbox's inability to open loopback sockets or run Docker — none is a code assertion. CI remains the source of truth for those suites.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the full-suite failure totals.

The entry reports 8 failures + 304 errors (312 total), but the listed categories sum to 318 (297 + 16 + 5). Please recheck the Surefire aggregation and make the totals consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/changelog.md` at line 90, Reconcile the full-suite summary in the
changelog entry by rechecking the Surefire aggregation and correcting the
reported failures, errors, and category counts so the totals are mathematically
consistent. Update the surrounding test-count statement if necessary, while
preserving the explanation that these are sandbox infrastructure issues rather
than assertion failures.

Comment thread src/test/java/ai/labs/eddi/engine/lifecycle/internal/LifecycleManagerTest.java Outdated
…n as failure

Clears the four follow-ups recorded after the merge, plus three CodeRabbit
review comments. Each was pre-existing; together they meant responseValidation
— a headline feature of this PR — silently did nothing on the paths it is most
likely to be configured for. Every change is TDD'd: test written first and
observed failing.

- Streaming now derives `warning` from finishReason (LENGTH -> truncated,
  CONTENT_FILTER -> content_filter), mirroring LegacyChatExecutor. Previously
  onTruncation/onContentFilter could never fire on a streaming task.

- CascadeResult and the internal StepResult now carry responseMetadata, so the
  winning step's warning/streamingTimeout/finishReason reach LlmTask. With
  modelCascade and responseValidation both enabled, only onEmpty and onRefusal
  could fire before this; a truncated or filtered answer reached the user even
  with action "error".

- A live-streamed step that hits its timeout is treated as failed rather than
  as a candidate: executeCapturing returns partial text instead of throwing, so
  such a step was accepted on equal footing with a real answer and, being last,
  beat a good earlier one. It no longer becomes bestSoFar, escalates when not
  last, and falls back to bestSoFar when last. This required executeCapturing to
  honour streamingTimeoutSeconds, which it ignored by passing task = null. Retry
  is still deliberately not applied on the cascade path — escalation is the
  cascade's retry, and stacking both multiplies spend.

- An interrupted streaming attempt no longer returns an empty success. Neither
  timedOut nor errorRef was set on InterruptedException, so a cancelled call was
  reported as a completed answer — contradicting AgentOrchestrator, which treats
  a set interrupt flag as a hard abort. It now records streamingInterrupted,
  salvages partial text with a warning on the LlmTask path, and otherwise throws.
  It is never retried; retrying would ignore the cancellation.

- Cascade failures are diagnosable: the retry wrapper throws a generic "failed
  after N attempts", and the cascade recorded only e.getMessage(), so a fully
  failed cascade read identically for a rate limit, an auth failure and a network
  outage. describeFailure() appends the root cause via a bounded chain walk.

- New CascadingModelExecutorErrorPathTest restores coverage lost when
  CascadingModelExecutorExtendedTest was deleted and CascadingModelExecutorCoverageTest
  was rewritten on main: last-step timeout with and without a bestSoFar, timeout
  escalation events, exception-cause handling, aggregated all-steps-failed errors,
  and enableInAgentMode=false never consulting the orchestrator.

- Review nits: replaced eight inline fully-qualified names in LifecycleManagerTest
  with top-level imports, and corrected a changelog verification claim whose
  per-bucket counts were measured over a surefire directory that still held XML
  from earlier targeted runs.

Verified on a clean run of the final code: 11,658 tests, 8 failures, 287 errors,
zero assertion failures — every failing case carries a blocked-loopback or
socket-selector message from this sandbox. CI remains the authority for the
socket- and Docker-dependent suites.
…ecutorErrorPathTest

The executor(...) helper took an ITemplatingEngineStub it never used, and that
marker interface existed only to make the argument look deliberate — a
readability device introduced in the previous commit that conveyed nothing.
Removed both; the seven call sites now read executor(registry).

Flagged by github-code-quality on PR #593. No behaviour change; the 7 tests in
the class still pass.
@ginccc
ginccc merged commit 70091de into main Jul 20, 2026
23 checks passed
@ginccc
ginccc deleted the feat/error-handling-recovery branch July 20, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants