This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new e42d7b043 fix(ai): reject failed Claude prompt enhancement completions 
(#6681)
e42d7b043 is described below

commit e42d7b0436d0aae70ff18b0355062b55cbbf46e5
Author: 健康宝 <[email protected]>
AuthorDate: Thu Oct 15 19:08:05 2026 +0800

    fix(ai): reject failed Claude prompt enhancement completions (#6681)
---
 .../studio/ops/ai/ClaudeCodeAgentProvider.java     | 20 +++++++++++-
 .../studio/ops/ai/ClaudeCodeAgentProviderTest.java | 36 ++++++++++++++++++++++
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
index 08c3d1a3d..22b3b003d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
@@ -43,6 +43,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
 /**
@@ -60,6 +61,8 @@ public class ClaudeCodeAgentProvider extends CliAgentProvider 
{
 
     public static final String ENGINE = "claude-code";
     private static final String BINARY = "claude";
+    /** Mirrors the parser's success subtype for the text-only channel's own 
check. */
+    private static final String RESULT_SUBTYPE_SUCCESS = "success";
     private static final String COMPATIBLE_MODE_SUFFIX = "/compatible-mode/v1";
     private static final String ANTHROPIC_APP_SUFFIX = "/apps/anthropic";
     private static final long STREAM_TIMEOUT_SECONDS = 300;
@@ -246,19 +249,34 @@ public class ClaudeCodeAgentProvider extends 
CliAgentProvider {
         List<String> command = withStreamFormat(buildCommand(config, prompt, 
modelOverride));
         ClaudeCodeStreamParser parser = new ClaudeCodeStreamParser();
         AtomicBoolean emitted = new AtomicBoolean(false);
+        AtomicReference<String> resultSubtype = new AtomicReference<>();
         SpawnResult spawn = spawn(command, childEnv(config), 
streamTimeoutSeconds(), null, line -> {
             for (AgentEvent event : parser.parseLine(line)) {
                 if (event instanceof AgentEvent.TextDelta delta && 
!delta.content().isEmpty()) {
                     emitted.set(true);
                     tokenConsumer.accept(delta.content());
+                } else if (event instanceof AgentEvent.ResultMeta result) {
+                    resultSubtype.set(result.subtype());
                 }
             }
         }, null);
-        if (spawn.exitCode() != 0 && !emitted.get()) {
+        // A partial rewrite that already streamed is not a completion: the
+        // enhancer only falls back to the raw prompt when this channel
+        // throws, so accepting the truncated rewrite replaced the user's
+        // message with the CLI's failure residue. A terminal result frame
+        // with a failure subtype fails the same way even at exit code 0 —
+        // the same rule streamEvents applies through the event channel.
+        if (spawn.exitCode() != 0) {
             throw new LlmGatewayException(502, "llm.provider.cli_error",
                     binaryName() + " CLI failed: " + 
describeStderr(spawn.stderr()),
                     "Check the provider credentials, base URL and model 
name.");
         }
+        String subtype = resultSubtype.get();
+        if (subtype != null && !RESULT_SUBTYPE_SUCCESS.equals(subtype)) {
+            throw new LlmGatewayException(502, "llm.provider.cli_error",
+                    binaryName() + " CLI reported result subtype " + subtype,
+                    "Check the provider credentials, base URL and model 
name.");
+        }
         if (!emitted.get() && StringUtils.hasText(parser.fallbackText())) {
             tokenConsumer.accept(parser.fallbackText());
         }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProviderTest.java
index 826eeaf44..70fb260d3 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProviderTest.java
@@ -58,6 +58,42 @@ class ClaudeCodeAgentProviderTest {
         assertThat(tokens).containsExactly("done");
     }
 
+    @Test
+    void streamRejectsAPartialRewriteThatEndedInFailureTest() {
+        // A result frame carrying text followed by a non-zero exit used to be
+        // accepted because text had already streamed; the enhancer then kept
+        // the truncated rewrite instead of falling back to the raw prompt.
+        TestClaudeCodeAgentProvider provider = new 
TestClaudeCodeAgentProvider(List.of(
+                "sh", "-c", "printf 
eyJ0eXBlIjoicmVzdWx0Iiwic3VidHlwZSI6ImVycm9yX2R1cmluZ19leGVjdXRpb24i"
+                        + 
"LCJpc19lcnJvciI6dHJ1ZSwicmVzdWx0IjoicGFydGlhbCByZXdyaXRlIn0= | base64 -d; "
+                        + "echo cli died >&2; exit 3"), 30);
+        List<String> tokens = new ArrayList<>();
+
+        assertThatThrownBy(() -> 
provider.stream(LlmConfigVO.builder().build(), "prompt", null, tokens::add))
+                .isInstanceOfSatisfying(LlmGatewayException.class, exception 
-> {
+                    assertThat(exception.getStatusCode()).isEqualTo(502);
+                    
assertThat(exception.getCode()).isEqualTo("llm.provider.cli_error");
+                    assertThat(exception.getMessage()).contains("cli died");
+                });
+    }
+
+    @Test
+    void streamRejectsAFailedResultSubtypeDespiteExitZeroTest() {
+        // Exit code 0 with a failure result frame is still a failure for the
+        // text channel, mirroring what the event channel does with the frame.
+        TestClaudeCodeAgentProvider provider = new 
TestClaudeCodeAgentProvider(List.of(
+                "sh", "-c", "printf 
eyJ0eXBlIjoicmVzdWx0Iiwic3VidHlwZSI6ImVycm9yX21heF90dXJucyIsImlzX2Vycm9y"
+                        + 
"Ijp0cnVlLCJyZXN1bHQiOiJ4Iiwic2Vzc2lvbl9pZCI6InMtMSIsImR1cmF0aW9uX21zIjo1LCJ1c2FnZSI6"
+                        + 
"eyJpbnB1dF90b2tlbnMiOjMsIm91dHB1dF90b2tlbnMiOjR9fQ== | base64 -d"), 30);
+
+        assertThatThrownBy(() -> 
provider.stream(LlmConfigVO.builder().build(), "prompt", null, token -> { }))
+                .isInstanceOfSatisfying(LlmGatewayException.class, exception 
-> {
+                    assertThat(exception.getStatusCode()).isEqualTo(502);
+                    
assertThat(exception.getCode()).isEqualTo("llm.provider.cli_error");
+                    
assertThat(exception.getMessage()).contains("error_max_turns");
+                });
+    }
+
     @Test
     void streamShouldBoundStderrIncludedInFailuresTest() {
         TestClaudeCodeAgentProvider provider = new TestClaudeCodeAgentProvider(

Reply via email to