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(