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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 54da210600f1 CAMEL-25784: camel-jbang - the AI tools wait for Camel to 
start, and read the log of the previous run (#28108)
54da210600f1 is described below

commit 54da210600f1636431c9c21df5f6f9909028daa2
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 21:57:45 2026 +0200

    CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read 
the log of the previous run (#28108)
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  10 ++
 .../apache/camel/dsl/jbang/core/commands/Run.java  |   3 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 115 +++++++++++++-
 .../core/commands/ai/IntegrationLauncher.java      |  19 ++-
 .../dsl/jbang/core/commands/ai/LogFileReader.java  |  13 +-
 .../camel/dsl/jbang/core/common/PreviousRun.java   |   9 +-
 .../jbang/core/commands/ai/AuthoringToolsTest.java |  24 ++-
 .../core/commands/ai/PreviousRunToolsTest.java     | 168 +++++++++++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |   9 +-
 .../core/commands/mcp/AuthoringToolsTest.java      |  18 ++-
 10 files changed, 364 insertions(+), 24 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 884216a6a6a1..3299d0902c77 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1866,3 +1866,13 @@ integration, with `~/.camel/<name>-previous.json`, which 
says when and how the r
 ends replaces both, and at most the last 10 MB of a log are kept. `camel log 
<name> --previous` prints the log, like
 `kubectl logs --previous` in Kubernetes. The log of an integration that was 
killed is kept the same way, the next time a
 previous run is looked up. `camel dirty` does not count these files.
+
+=== camel-jbang - the AI tools wait for Camel to start, and read the log of 
the previous run
+
+* `camel_run` and `camel_control restart` answer that the integration started 
once its CamelContext has started. They
+  answered as soon as the status file of the integration appeared, which 
happens while Camel starts and before the
+  routes start. An integration whose route then failed to start was answered 
as started, and stopped right after.
+  Now the answer is the failure, with its cause.
+* `camel_get_log` of an integration that is not running answers with the log 
of its previous run (see
+  `camel log --previous`), and its message says how that run ended. 
`previous=true` asks for that log also when the
+  integration runs again. `camel_get_errors` of an integration that is not 
running says how its previous run ended.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index e133dd3a5675..a778e6aaef1d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -3083,7 +3083,8 @@ public class Run extends CamelCommand {
             exitCode = code == Integer.MIN_VALUE ? CliExitCode.OK : code;
             return exitCode;
         } catch (Exception | Error e) {
-            // such as a route that failed to start
+            // such as a route that failed to start, or an Error such as a 
NoClassDefFoundError of a missing dependency:
+            // only noted as why the run ended, and thrown again
             failure = e.getMessage() != null ? e.getMessage() : 
e.getClass().getName();
             exitCode = new 
CliExecutionExceptionHandler(getMain()).getExitCode(e);
             throw e;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index de55433771e8..b9d7f2d8302e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -40,8 +40,10 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -323,15 +325,31 @@ public final class AuthoringTools {
 
         registry.accept(tool("camel_get_log",
                 "Recent log records of a running integration, newest first, 
with optional filtering; stack traces "
-                                              + "only with details.")
+                                              + "only with details. An 
integration that is not running gets the log of "
+                                              + "its previous run, such as one 
that failed to start.")
                 .param("name", "string", NAME_DESC, false)
                 .param("limit", "integer", "Maximum records to return (default 
50)", false)
                 .param("filter", "string", "Case-insensitive substring filter 
on the message", false)
                 .param("level", "string", "Only this log level (INFO, WARN, 
ERROR, DEBUG, TRACE)", false)
                 .param("details", "boolean", "Include the stack traces 
(default false)", false)
+                .param("previous", "boolean",
+                        "The log of the previous run, the last run of the 
integration that ended (default false)", false)
                 .core(true)
                 .executor((ctx, args) -> {
-                    RuntimeHelper.ProcessInfo p = selectProcess(ctx, args);
+                    if (bool(args, "previous", false)) {
+                        return previousLog(ctx, args, null).toJson();
+                    }
+                    RuntimeHelper.ProcessInfo p;
+                    try {
+                        p = selectProcess(ctx, args);
+                    } catch (ToolExecutionException e) {
+                        // not running: the log of its previous run tells why 
(CAMEL-25784)
+                        return previousLog(ctx, args, e).toJson();
+                    }
+                    if (p == null && hasPreviousRun(ctx.pid())) {
+                        // the process selected before is gone, such as one 
that died at startup
+                        return previousLog(ctx, args, null).toJson();
+                    }
                     return LogFileReader.read(ctx.pid(), p != null ? p.name() 
: null, integer(args, "limit", 50),
                             args.get("filter"), args.get("level"), bool(args, 
"details", false)).toJson();
                 }));
@@ -342,7 +360,16 @@ public final class AuthoringTools {
                 .param("name", "string", NAME_DESC, false)
                 .core(true)
                 .executor((ctx, args) -> {
-                    selectProcess(ctx, args);
+                    RuntimeHelper.ProcessInfo p;
+                    try {
+                        p = selectProcess(ctx, args);
+                    } catch (ToolExecutionException e) {
+                        // not running: how its previous run ended tells why 
(CAMEL-25784)
+                        throw notRunning(ctx, args, e);
+                    }
+                    if (p == null && hasPreviousRun(ctx.pid())) {
+                        throw notRunning(ctx, args, new 
ToolExecutionException("pid " + ctx.pid() + " is not running"));
+                    }
                     JsonObject errors = ctx.readErrorFile();
                     return errors != null ? unescapeBodies(errors).toJson() : 
"No errors captured.";
                 }));
@@ -2114,6 +2141,88 @@ public final class AuthoringTools {
         }
     }
 
+    /**
+     * The log of the previous run of the integration (CAMEL-25784): the last 
run of it that ended, such as one that
+     * failed to start, found by the name or pid given, else by the pid 
selected before. Its message says how that run
+     * ended.
+     *
+     * @param  notRunning             why the integration was not found 
running, or null when the previous run was asked
+     *                                for
+     * @throws ToolExecutionException when no previous run matches, or more 
than one (notRunning, when given)
+     */
+    private static JsonObject previousLog(ToolContext ctx, Map<String, String> 
args, ToolExecutionException notRunning) {
+        PreviousRun.Info info = previousRun(ctx, args, notRunning);
+        JsonObject answer = new JsonObject();
+        answer.put("message", (notRunning != null ? info.name() + " is not 
running. " : "")
+                              + "This is the log of its previous run" + 
(info.pid() > 0 ? " (pid " + info.pid() + ")" : "")
+                              + ", which " + info.howItEnded());
+        JsonObject previous = new JsonObject();
+        previous.put("name", info.name());
+        if (info.pid() > 0) {
+            previous.put("pid", info.pid());
+        }
+        previous.put("ended", info.endedAt());
+        if (info.exitCode() != null) {
+            previous.put("exitCode", info.exitCode());
+        }
+        if (info.signal() != null) {
+            previous.put("signal", info.signal());
+        }
+        if (info.failure() != null) {
+            previous.put("failure", info.failure());
+        }
+        answer.put("previous", previous);
+        answer.putAll(LogFileReader.read(info.log(), integer(args, "limit", 
50), args.get("filter"), args.get("level"),
+                bool(args, "details", false)));
+        return answer;
+    }
+
+    /**
+     * The error of an integration that is not running, with how its previous 
run ended when there is one.
+     */
+    private static ToolExecutionException notRunning(
+            ToolContext ctx, Map<String, String> args, ToolExecutionException 
notRunning) {
+        PreviousRun.Info info;
+        try {
+            info = previousRun(ctx, args, notRunning);
+        } catch (ToolExecutionException e) {
+            return notRunning;
+        }
+        return new ToolExecutionException(
+                notRunning.getMessage() + ". Its previous run"
+                                          + (info.pid() > 0 ? " (pid " + 
info.pid() + ")" : "") + " " + info.howItEnded()
+                                          + ". camel_get_log shows its log.");
+    }
+
+    // the one previous run of the name or pid given, else of the pid selected 
before
+    private static PreviousRun.Info previousRun(
+            ToolContext ctx, Map<String, String> args, ToolExecutionException 
notRunning) {
+        String name = args.get("name");
+        String key = name != null && !name.isBlank() ? name : (ctx.pid() >= 0 
? Long.toString(ctx.pid()) : null);
+        List<PreviousRun.Info> found = key != null ? PreviousRun.find(key) : 
List.of();
+        if (found.size() == 1) {
+            return found.get(0);
+        }
+        if (notRunning != null) {
+            throw notRunning;
+        }
+        if (key == null) {
+            throw new ToolExecutionException("No integration selected: pass 
name (an integration name or pid)");
+        }
+        throw new ToolExecutionException(
+                found.isEmpty()
+                        ? "No previous run matches '" + key + "'"
+                        : "'" + key + "' matches " + found.size() + " previous 
runs: "
+                          + 
found.stream().map(PreviousRun.Info::name).collect(Collectors.joining(", "))
+                          + "; pass the name of one");
+    }
+
+    // whether a run of the pid has ended and kept its log
+    private static boolean hasPreviousRun(long pid) {
+        return pid >= 0 && ProcessHandle.of(pid).map(ph -> 
!ph.isAlive()).orElse(true)
+                && !PreviousRun.find(Long.toString(pid)).isEmpty();
+    }
+
     /**
      * Selects the process the call names, else keeps the selected one, else 
the only one running.
      *
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index b666032ab2b5..20c6f0733dd9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -168,7 +168,7 @@ public final class IntegrationLauncher {
                 return result;
             }
             RuntimeHelper.ProcessInfo info = findByPid(pid);
-            if (info != null) {
+            if (info != null && started(pid)) {
                 // the name the other tools find the process by: the one the 
caller gave, else the Camel context name
                 // (the source file name without its extensions); the pid is 
in the result as well
                 String started = runName != null && !runName.isBlank()
@@ -284,6 +284,21 @@ public final class IntegrationLauncher {
         return cmd;
     }
 
+    /**
+     * Whether the CamelContext of the integration has started, as its status 
file says (CAMEL-25784). The CLI connector
+     * writes the status file while Camel starts, before the routes start, so 
a route that fails to start ends the
+     * process after the file has appeared: a run that was answered as started 
had died at startup. A status without a
+     * state (an older Camel) counts as started.
+     */
+    static boolean started(long pid) {
+        JsonObject status = RuntimeHelper.readStatus(pid);
+        if (status == null) {
+            return false;
+        }
+        String state = status.get("context") instanceof JsonObject context ? 
context.getString("state") : null;
+        return state == null || "Started".equals(state);
+    }
+
     private static RuntimeHelper.ProcessInfo findByPid(long pid) {
         for (RuntimeHelper.ProcessInfo p : RuntimeHelper.discoverProcesses()) {
             if (p.pid() == pid) {
@@ -623,7 +638,7 @@ public final class IntegrationLauncher {
             Path output = launched.log();
             long deadline = System.currentTimeMillis() + STARTUP_TIMEOUT_MS;
             while (System.currentTimeMillis() < deadline && p.isAlive()) {
-                if (findByPid(p.pid()) != null) {
+                if (findByPid(p.pid()) != null && started(p.pid())) {
                     removeWhenEnded(p, output);
                     return "Restarted " + (name != null ? name : "pid " + pid) 
+ " as pid " + p.pid();
                 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
index 3466f5e4ca21..f67c1dc3cd8e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
@@ -118,7 +118,18 @@ public final class LogFileReader {
      * @return         lines (newest first), totalLines and returnedLines, or 
an error when there is no log file
      */
     public static JsonObject read(long pid, String name, int limit, String 
filter, String level, boolean details) {
-        Path file = logFile(pid, name);
+        return read(logFile(pid, name), limit, filter, level, details);
+    }
+
+    /**
+     * @param  file    the log file, such as the log of the previous run of an 
integration
+     * @param  limit   maximum records to return
+     * @param  filter  case-insensitive substring the message or detail must 
contain; null for all
+     * @param  level   only records of this level (INFO, WARN, ERROR, DEBUG, 
TRACE); null for all
+     * @param  details whether a stack trace stays in its record
+     * @return         lines (newest first), totalLines and returnedLines, or 
an error when there is no log file
+     */
+    public static JsonObject read(Path file, int limit, String filter, String 
level, boolean details) {
         JsonObject result = new JsonObject();
         result.put("file", file.toString());
         if (!Files.isRegularFile(file)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
index c6343cb1a8c6..03e7bbac8f80 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
@@ -104,11 +104,18 @@ public final class PreviousRun {
             return "The previous run of " + name + (pid > 0 ? " (pid " + pid + 
")" : "") + " " + howItEnded();
         }
 
+        /**
+         * When the run ended, in local time, such as <tt>2026-10-19 
10:32:01</tt>.
+         */
+        public String endedAt() {
+            return TIME.format(Instant.ofEpochMilli(ended));
+        }
+
         /**
          * How the run ended: when, with which exit code, and why when it 
failed.
          */
         public String howItEnded() {
-            StringBuilder sb = new StringBuilder("ended at 
").append(TIME.format(Instant.ofEpochMilli(ended)));
+            StringBuilder sb = new StringBuilder("ended at 
").append(endedAt());
             long ago = System.currentTimeMillis() - ended;
             if (ago >= 0) {
                 sb.append(" (").append(TimeUtils.printDuration(ago, 
false)).append(" ago)");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index ef477780cfcb..f684fb684dec 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -363,14 +364,21 @@ class AuthoringToolsTest {
     }
 
     @Test
-    void runtimeToolsSayWhatToDoWithoutAnIntegration() {
-        ToolExecutionException e = assertThrows(ToolExecutionException.class,
-                () -> ToolRegistry.execute("camel_get_errors", new 
ToolContext(), Map.of("name", "no-such-app-xyz")));
-        assertTrue(e.getMessage().contains("no-such-app-xyz"), e.getMessage());
-        e = assertThrows(ToolExecutionException.class,
-                () -> ToolRegistry.execute("camel_control", new ToolContext(), 
Map.of("action", "stop", "name",
-                        "no-such-app-xyz")));
-        assertTrue(e.getMessage().contains("no-such-app-xyz"), e.getMessage());
+    void runtimeToolsSayWhatToDoWithoutAnIntegration(@TempDir Path home) {
+        // a home of its own: the tools look for the previous run of an 
integration that is not running
+        Path previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        try {
+            ToolExecutionException e = 
assertThrows(ToolExecutionException.class,
+                    () -> ToolRegistry.execute("camel_get_errors", new 
ToolContext(), Map.of("name", "no-such-app-xyz")));
+            assertTrue(e.getMessage().contains("no-such-app-xyz"), 
e.getMessage());
+            e = assertThrows(ToolExecutionException.class,
+                    () -> ToolRegistry.execute("camel_control", new 
ToolContext(), Map.of("action", "stop", "name",
+                            "no-such-app-xyz")));
+            assertTrue(e.getMessage().contains("no-such-app-xyz"), 
e.getMessage());
+        } finally {
+            CommandLineHelper.useHomeDir(previousHome.toString());
+        }
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PreviousRunToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PreviousRunToolsTest.java
new file mode 100644
index 000000000000..e27dbf2684d4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PreviousRunToolsTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-25784: the AI tools read the log of the previous run of an 
integration that is not running, such as one that
+ * died at startup, and camel_run and camel_control restart wait until Camel 
has started.
+ */
+class PreviousRunToolsTest {
+
+    // a pid no process has (above the highest pid of Linux and macOS)
+    private static final long GONE_PID = 999_999_991L;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+
+    @BeforeEach
+    void useHome() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+    }
+
+    @AfterEach
+    void restoreHome() {
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void theLogOfAnIntegrationThatIsNotRunningIsTheLogOfItsPreviousRun() 
throws Exception {
+        keepPreviousRun("orders", GONE_PID);
+
+        JsonObject log = call("camel_get_log", new ToolContext(), 
Map.of("name", "orders"));
+
+        assertThat(log.getString("message"))
+                .startsWith("orders is not running. This is the log of its 
previous run (pid " + GONE_PID
+                            + "), which ended at ")
+                .endsWith(" with exit code 70: Failed to start route: route1 
because: boom");
+        JsonObject previous = log.getMap("previous");
+        assertThat(previous.getString("name")).isEqualTo("orders");
+        assertThat(previous.getInteger("exitCode")).isEqualTo(70);
+        assertThat(previous.getString("failure")).isEqualTo("Failed to start 
route: route1 because: boom");
+        assertThat(log.getString("file")).endsWith("orders-previous.log");
+        // newest first
+        JsonObject newest = (JsonObject) 
log.getCollection("lines").iterator().next();
+        assertThat(newest.getString("level")).isEqualTo("ERROR");
+        assertThat(newest.getString("message")).contains("Failed to start 
route route1");
+    }
+
+    @Test
+    void theLogOfThePreviousRunCanBeAskedFor() throws Exception {
+        keepPreviousRun("orders", GONE_PID);
+
+        JsonObject log = call("camel_get_log", new ToolContext(), 
Map.of("name", "orders", "previous", "true"));
+
+        assertThat(log.getString("message"))
+                .startsWith("This is the log of its previous run (pid " + 
GONE_PID + "), which ended at ");
+    }
+
+    @Test
+    void theIntegrationSelectedBeforeThatIsGoneHasTheLogOfItsPreviousRun() 
throws Exception {
+        keepPreviousRun("orders", GONE_PID);
+        ToolContext ctx = new ToolContext();
+        // such as the pid camel_run answered with, of an integration that 
then died at startup
+        ctx.selectProcess(GONE_PID);
+
+        JsonObject log = call("camel_get_log", ctx, Map.of());
+
+        assertThat(log.getString("message"))
+                .startsWith("This is the log of its previous run (pid " + 
GONE_PID + "), which ended at ");
+    }
+
+    @Test
+    void anIntegrationWithoutPreviousRunIsNotRunning() throws Exception {
+        keepPreviousRun("orders", GONE_PID);
+
+        assertThatThrownBy(() -> call("camel_get_log", new ToolContext(), 
Map.of("name", "payments")))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessage("No running Camel process matches 'payments'; 
list_processes shows what runs");
+        assertThatThrownBy(() -> call("camel_get_log", new ToolContext(), 
Map.of("name", "payments", "previous", "true")))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessage("No previous run matches 'payments'");
+    }
+
+    @Test
+    void theErrorsOfAnIntegrationThatIsNotRunningSayHowItsPreviousRunEnded() 
throws Exception {
+        keepPreviousRun("orders", GONE_PID);
+
+        assertThatThrownBy(() -> ToolRegistry.execute("camel_get_errors", new 
ToolContext(), Map.of("name", "orders")))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageStartingWith("No running Camel process matches 
'orders'; list_processes shows what runs."
+                                        + " Its previous run (pid " + GONE_PID 
+ ") ended at ")
+                .hasMessageEndingWith(
+                        " with exit code 70: Failed to start route: route1 
because: boom. camel_get_log shows its log.");
+    }
+
+    @Test
+    void anIntegrationHasStartedWhenItsCamelContextHas() throws Exception {
+        // the CLI connector writes the status file while Camel starts, before 
the routes start
+        status(GONE_PID, "Starting");
+        assertThat(IntegrationLauncher.started(GONE_PID)).isFalse();
+        status(GONE_PID, "Started");
+        assertThat(IntegrationLauncher.started(GONE_PID)).isTrue();
+        // an older Camel without the state
+        status(GONE_PID, null);
+        assertThat(IntegrationLauncher.started(GONE_PID)).isTrue();
+        // no status file yet
+        assertThat(IntegrationLauncher.started(GONE_PID + 1)).isFalse();
+    }
+
+    // a run of the name that failed to start
+    private static void keepPreviousRun(String name, long pid) throws 
Exception {
+        String record = " " + pid + " --- [           main] 
org.apache.camel.main.MainSupport   : ";
+        Path log = CommandLineHelper.getCamelDir().resolve("4242.log");
+        Files.write(log, List.of("2026-10-19 12:47:11.498  INFO" + record + 
"Apache Camel 4.24.0 is starting",
+                "2026-10-19 12:47:11.901 ERROR" + record + "Failed to start 
route route1"));
+        PreviousRun.keep(log, name, pid, 70, "Failed to start route: route1 
because: boom");
+    }
+
+    private static void status(long pid, String state) throws Exception {
+        JsonObject context = new JsonObject();
+        context.put("name", "orders");
+        if (state != null) {
+            context.put("state", state);
+        }
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json"), root.toJson());
+    }
+
+    private static JsonObject call(String tool, ToolContext ctx, Map<String, 
String> args) throws Exception {
+        return (JsonObject) 
Jsoner.deserialize(String.valueOf(ToolRegistry.execute(tool, ctx, new 
HashMap<>(args))));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 83a8b057721e..226bfbaf4cee 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -284,15 +284,18 @@ public class AuthoringTools {
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Recent log records of a running integration, newest 
first, with optional filtering; stack "
-                        + "traces only with details.")
+                        + "traces only with details. An integration that is 
not running gets the log of its previous "
+                        + "run, such as one that failed to start.")
     public JsonObject camel_get_log(
             @ToolArg(description = NAME_DESC, required = false) String name,
             @ToolArg(description = "Maximum records to return (default 50)", 
required = false) Integer limit,
             @ToolArg(description = "Case-insensitive substring filter on the 
message", required = false) String filter,
             @ToolArg(description = "Only this log level (INFO, WARN, ERROR, 
DEBUG, TRACE)", required = false) String level,
-            @ToolArg(description = "Include the stack traces (default false)", 
required = false) Boolean details) {
+            @ToolArg(description = "Include the stack traces (default false)", 
required = false) Boolean details,
+            @ToolArg(description = "The log of the previous run, the last run 
of the integration that ended (default false)",
+                     required = false) Boolean previous) {
         return call("camel_get_log", args("name", name, "limit", limit, 
"filter", filter, "level", level,
-                "details", details));
+                "details", details, "previous", previous));
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 54c245be834b..9559b8f780ed 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -34,6 +34,7 @@ import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -128,11 +129,18 @@ class AuthoringToolsTest {
     }
 
     @Test
-    void anUnknownIntegrationNameIsAnError() {
-        assertThatThrownBy(() -> tools.camel_eval_expression("${body}", null, 
"camel", "no-such-app-xyz-1"))
-                
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
-        assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1", 
null, null, null, null))
-                
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
+    void anUnknownIntegrationNameIsAnError(@TempDir Path home) {
+        // a home of its own: the log tool looks for the previous run of an 
integration that is not running
+        Path previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        try {
+            assertThatThrownBy(() -> tools.camel_eval_expression("${body}", 
null, "camel", "no-such-app-xyz-1"))
+                    
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
+            assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1", 
null, null, null, null, null))
+                    
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
+        } finally {
+            CommandLineHelper.useHomeDir(previousHome.toString());
+        }
     }
 
     @Test

Reply via email to