You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "davsclaus (via GitHub)" <gi...@apache.org> on 2026/10/19 11:01:23 UTC

[PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

davsclaus opened a new pull request, #28108:
URL: https://github.com/apache/camel/pull/28108

   [CAMEL-25784](https://issues.apache.org/jira/browse/CAMEL-25784), the part for the AI tools. **This PR builds on #28107** (the first commit here). Once #28107 is merged, I will rebase this PR so it only has its own commit.
   
   In a benchmark of Oct 18-19 2026, an app died at startup after a restart in 4 of the first 6 runs of one example (contracts/rest-code-first, a code-first REST DSL with an API description, which failed on CAMEL-25768). The trace of one run:
   
   ```
   camel_control restart contracts-rest-code-first -> Restarted contracts-rest-code-first as pid 46486
   camel_get_log contracts-rest-code-first        -> No running Camel process matches 'contracts-rest-code-first'
   camel_run                                      -> status started, pid 46489
   camel_get_log 46489                            -> No running Camel process matches '46489'
   ... (two more starts, the same answers)
   ```
   
   The model restarted the app three times and never saw the error.
   
   ## The cause
   
   `camel_run` and `camel_control restart` answered "started" as soon as the status file of the new process appeared. The CLI connector writes that file while Camel starts, before the routes start. A run of that example shows it:
   
   ```
   status file appears at 1.74 s: context state Initializing (phase 2)
   process exits at 2.82 s with exit code 70: Cannot find RestApiProcessorFactory in Registry or classpath
   ```
   
   So both tools answered "started" for a process that died a second later, and the tools that came after found no process.
   
   ## What
   
   - **`camel_run` and `camel_control restart` wait until the CamelContext has started**, as the status file says (`context.state` is `Started`). A process that exits while it starts now gets the answer the tools already give for that: the failure with its cause and the output. A status without a state (an older Camel) counts as started, as before.
   - **`camel_get_log` of an integration that is not running answers with the log of its previous run** (#28107 keeps it, as `camel log --previous` shows it). It finds the run by the name or pid given, else by the pid selected before (such as the pid `camel_run` answered with). The message says how that run ended.
     The answer also has `previous` (name, pid, ended, exitCode, failure) and the records, as for a running integration. The answer for a run of that example on this branch (records cut):
     ```json
     {"message":"e2e-rest is not running. This is the log of its previous run (pid 40927), which ended at 2026-10-19 12:58:17 (2m28s ago) with exit code 70: Failed to start route: route1 because: java.lang.IllegalStateException: Cannot find RestApiProcessorFactory in Registry or classpath (such as the camel-openapi-java component)",
      "previous":{"name":"e2e-rest","pid":40927,"ended":"2026-10-19 12:58:17","exitCode":70,"failure":"Failed to start route: route1 because: ..."},
      "file":"~/.camel/e2e-rest-previous.log",
      "lines":[{"time":"12:58:17.301","level":"ERROR","logger":"MainSupport","message":"Error starting Camel: org.apache.camel.FailedToStartRouteException: Failed to start route: route1 because: ...","detailLines":75}, ...],
      "totalLines":168,"returnedLines":3}
     ```
   - **`previous=true`** asks `camel_get_log` for that log also when the integration runs again, such as after a restart. The camel-jbang-mcp server has the same argument.
   - **`camel_get_errors` of an integration that is not running** says how its previous run ended, and that `camel_get_log` shows its log:
     ```
     No running Camel process matches 'e2e-rest'; list_processes shows what runs. Its previous run (pid 40927) ended at 2026-10-19 12:58:17 (2m28s ago) with exit code 70: Failed to start route: route1 because: java.lang.IllegalStateException: Cannot find RestApiProcessorFactory in Registry or classpath (such as the camel-openapi-java component). camel_get_log shows its log.
     ```
   
   A name that matches no previous run gets the same error as before.
   
   ## Tests
   
   - New `PreviousRunToolsTest` (6): the log of the previous run by name, asked for with `previous`, and by the pid selected before; a name without a previous run; `camel_get_errors`; and when an integration has started (Starting, Started, no state, no status).
   - Two tests that looked at the real `~/.camel` (in camel-jbang-core and camel-jbang-mcp) now use a home directory of their own, as the tools now look for previous runs there.
   - All tests of camel-jbang-core pass (2335, 2 skipped, and 237), and of camel-jbang-mcp (503).
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   
   https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #28108:
URL: https://github.com/apache/camel/pull/28108#discussion_r4313173737


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -2119,6 +2146,85 @@ private static void applyVersion(ToolContext ctx, Map<String, String> args) {
      *
      * @throws ToolExecutionException when no process can be selected
      */

Review Comment:
   🐛 **Orphaned javadoc:** the `*/` on this line closes the `selectProcess()` javadoc that was just before (which belonged to `selectProcess()` ~80 lines further down). The new `previousLog()` javadoc opens immediately after on the next line, so the file now has two consecutive `/** … */` blocks with nothing between them — the first one is dangling. The `selectProcess()` javadoc that ends here should be moved back to sit directly above `selectProcess()` itself.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -2119,6 +2146,85 @@ private static void applyVersion(ToolContext ctx, Map<String, String> args) {
      *
      * @throws ToolExecutionException when no process can be selected
      */
+    /**
+     * 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.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;

Review Comment:
   💡 **Nested ternary:** the chained ternary is hard to parse at a glance. Adding parentheses around the inner expression makes the precedence explicit and helps readers:
   
   ```suggestion
           String key = name != null && !name.isBlank() ? name : (ctx.pid() >= 0 ? Long.toString(ctx.pid()) : null);
   ```



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java:
##########
@@ -3052,26 +3054,63 @@ protected int runCustomCamelVersion(KameletMain main) throws Exception {
 
     protected int runKameletMain(KameletMain main) throws Exception {
         Recording jfrRecording = startJfrRecording();
+        PreviousRun.Keeper previousLog = null;
+        if (logFile != null && keepsPreviousLog()) {
+            // CAMEL-25784: the log is kept as the previous log of the integration when the run ends, and so are the
+            // logs that runs which were killed left behind
+            PreviousRun.keepOrphans();
+            previousLog = new PreviousRun.Keeper(logFile, () -> runName(main));
+        }
+        Integer exitCode = null;
+        String failure = null;
         try {
             main.start();
             if (hasNoRoutes(main)) {
                 // it would run doing nothing: a file without routes, or a mistake in it
-                printer().printErr(files.isEmpty()
-                        ? "No routes found" : "No routes found in: " + String.join(", ", files));
+                failure = files.isEmpty() ? "No routes found" : "No routes found in: " + String.join(", ", files);
+                printer().printErr(failure);
                 main.stop();
-                return CliExitCode.FAILURE;
+                exitCode = CliExitCode.FAILURE;
+                return exitCode;
             }
             main.run();
+            int code = main.getExitCode();
+            // no exit code is set when Camel is stopped from outside (such as camel stop)
+            exitCode = code == Integer.MIN_VALUE ? CliExitCode.OK : code;
+            return exitCode;
+        } catch (Exception | Error e) {

Review Comment:
   ❓ **`catch (Exception | Error e)`:** catching `Error` is unusual — the comment only explains the `Exception` case ("such as a route that failed to start"). `Error` subtypes like `OutOfMemoryError` or `StackOverflowError` are normally not meant to be caught and handled. Is this intentional? If there is a specific `Error` subtype you need to capture for the failure log (e.g. `AssertionError` in tests?), it would be clearer to list it explicitly or update the comment to explain why `Error` is included.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java:
##########
@@ -0,0 +1,430 @@
+/*
+ * 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.common;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The log of the previous run of an integration, as <tt>kubectl logs --previous</tt> shows the log of the previous
+ * instance of a container in Kubernetes.
+ * <p/>
+ * When <tt>camel run</tt> ends (it was stopped, it failed to start, or it stopped by itself), its log is kept as
+ * <tt>~/.camel/&lt;name&gt;-previous.log</tt>, by the name of the integration, with
+ * <tt>~/.camel/&lt;name&gt;-previous.json</tt>, which says when and how the run ended. The next run of that name that
+ * ends replaces both. Like Kubernetes, at most the last 10 MB of a log are kept.
+ * <p/>
+ * A process that is gone without keeping its log (it was killed, or its JVM crashed) leaves its
+ * <tt>~/.camel/&lt;pid&gt;.log</tt> behind. That log is kept for it, under the name in its status file, the next time
+ * the previous runs are looked up, and the next time an integration is run.
+ */
+public final class PreviousRun {
+
+    /**
+     * The end of a log that is kept, at most: Kubernetes keeps at most 10 MB of the log of a container by default.
+     */
+    static final long MAX_SIZE = 10L * 1024 * 1024;
+
+    /**
+     * How old the log of a process that is gone may get, when there is no name to keep it by, before it is removed.
+     */
+    static final long ORPHAN_AGE = TimeUnit.DAYS.toMillis(1);
+
+    /**
+     * What a process that is gone without keeping its log ended with, as far as is known.
+     */
+    static final String GONE = "the process was gone without stopping Camel (killed, or its JVM crashed)";
+
+    private static final String LOG_SUFFIX = "-previous.log";
+    private static final String INFO_SUFFIX = "-previous.json";
+    private static final Pattern PID_LOG = Pattern.compile("(\\d+)\\.log");
+    private static final int MAX_FAILURE = 500;
+    private static final DateTimeFormatter TIME
+            = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * A run that ended, and the log it kept.
+     *
+     * @param name      the name of the integration
+     * @param pid       the pid it ran as, 0 when not known
+     * @param directory the directory it ran in, or <tt>null</tt>
+     * @param started   when it started, in epoch millis, 0 when not known
+     * @param ended     when it ended, in epoch millis
+     * @param exitCode  the exit code, or <tt>null</tt> when not known (such as of a process that was killed)
+     * @param failure   why it ended, when it failed (such as the error it failed to start with), or <tt>null</tt>
+     * @param log       the log it kept
+     */
+    public record Info(String name, long pid, String directory, long started, long ended, Integer exitCode,
+            String failure, Path log) {
+
+        /**
+         * Which run it was, and how it ended, in one line.
+         */
+        public String describe() {
+            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(endedAt());
+            long ago = System.currentTimeMillis() - ended;
+            if (ago >= 0) {
+                sb.append(" (").append(TimeUtils.printDuration(ago, false)).append(" ago)");
+            }
+            if (exitCode != null) {
+                sb.append(" with exit code ").append(exitCode);
+            }
+            if (failure != null) {
+                sb.append(": ").append(failure);
+            }
+            return sb.toString();
+        }
+    }
+
+    private PreviousRun() {
+    }
+
+    /**
+     * Keeps the log of a run of this JVM that ended as the previous log of its name.
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, String failure) {
+        long started = ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+        return keep(log, new Info(
+                name, pid, System.getProperty("user.dir"), started, System.currentTimeMillis(),
+                exitCode, failure, null));
+    }
+
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, in place of the one kept before.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    static Path keep(Path log, Info info) {
+        if (log == null || info.name() == null || info.name().isBlank() || !Files.isRegularFile(log)) {
+            return null;
+        }
+        Path dir = CommandLineHelper.getCamelDir();
+        String file = fileName(info.name());
+        Path target = dir.resolve(file + LOG_SUFFIX);
+        try {
+            if (Files.size(log) > MAX_SIZE || !move(log, target)) {
+                copyEnd(log, target, MAX_SIZE);
+                try {
+                    Files.deleteIfExists(log);
+                } catch (IOException e) {
+                    // still open (Windows): removed when the process is gone (see keepOrphans)
+                }
+            }
+            JsonObject jo = new JsonObject();
+            jo.put("name", info.name());
+            if (info.pid() > 0) {
+                jo.put("pid", info.pid());
+            }
+            if (info.directory() != null) {
+                jo.put("directory", info.directory());
+            }
+            if (info.started() > 0) {
+                jo.put("started", info.started());
+            }
+            jo.put("ended", info.ended());
+            if (info.exitCode() != null) {
+                jo.put("exitCode", info.exitCode());
+            }
+            if (info.failure() != null) {
+                String failure = info.failure().strip();
+                jo.put("failure", failure.length() > MAX_FAILURE ? failure.substring(0, MAX_FAILURE) + "..." : failure);
+            }
+            Files.writeString(dir.resolve(file + INFO_SUFFIX), jo.toJson());

Review Comment:
   💡 **Non-atomic log/info pair:** the log file is moved/copied first (lines 162–164), then the JSON metadata file is written last (this line). If the JVM crashes between the two operations the log has been updated but the `.info` file still describes the previous run, making the pair inconsistent. Writing the JSON file *before* moving the log would be safer: a reader that finds no log but a stale info can treat it as incomplete, whereas a moved log with no info is ambiguous.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on PR #28108:
URL: https://github.com/apache/camel/pull/28108#issuecomment-6276206168

   Right, the description of `camel_get_errors` now says it, in the core tool and the camel-jbang-mcp server: "For an integration that is not running, it says how its previous run ended." The MCP docs table says it for both tools. Follow-up PR: #28127.
   
   _Claude Code on behalf of davsclaus_


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #28108:
URL: https://github.com/apache/camel/pull/28108#issuecomment-6265697877

   :star2: Thank you for your contribution to the Apache Camel project! :star2:
     :robot: CI automation will test this PR automatically.
   
     :camel: Apache Camel Committers, please review the following items:
   
     * First-time contributors **require MANUAL approval** for the GitHub Actions to run
     * You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
     * You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
     * Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
     
     :warning: Be careful when sharing logs. Review their contents before sharing them publicly.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #28108:
URL: https://github.com/apache/camel/pull/28108#discussion_r4315406625


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -2119,6 +2146,85 @@ private static void applyVersion(ToolContext ctx, Map<String, String> args) {
      *
      * @throws ToolExecutionException when no process can be selected
      */

Review Comment:
   Fixed in 6339cd749c68: the javadoc is back on `selectProcess()`.
   
   _Claude Code on behalf of Claus Ibsen_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -2119,6 +2146,85 @@ private static void applyVersion(ToolContext ctx, Map<String, String> args) {
      *
      * @throws ToolExecutionException when no process can be selected
      */
+    /**
+     * 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.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;

Review Comment:
   Fixed in 6339cd749c68 with the parentheses.
   
   _Claude Code on behalf of Claus Ibsen_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java:
##########
@@ -0,0 +1,430 @@
+/*
+ * 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.common;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The log of the previous run of an integration, as <tt>kubectl logs --previous</tt> shows the log of the previous
+ * instance of a container in Kubernetes.
+ * <p/>
+ * When <tt>camel run</tt> ends (it was stopped, it failed to start, or it stopped by itself), its log is kept as
+ * <tt>~/.camel/&lt;name&gt;-previous.log</tt>, by the name of the integration, with
+ * <tt>~/.camel/&lt;name&gt;-previous.json</tt>, which says when and how the run ended. The next run of that name that
+ * ends replaces both. Like Kubernetes, at most the last 10 MB of a log are kept.
+ * <p/>
+ * A process that is gone without keeping its log (it was killed, or its JVM crashed) leaves its
+ * <tt>~/.camel/&lt;pid&gt;.log</tt> behind. That log is kept for it, under the name in its status file, the next time
+ * the previous runs are looked up, and the next time an integration is run.
+ */
+public final class PreviousRun {
+
+    /**
+     * The end of a log that is kept, at most: Kubernetes keeps at most 10 MB of the log of a container by default.
+     */
+    static final long MAX_SIZE = 10L * 1024 * 1024;
+
+    /**
+     * How old the log of a process that is gone may get, when there is no name to keep it by, before it is removed.
+     */
+    static final long ORPHAN_AGE = TimeUnit.DAYS.toMillis(1);
+
+    /**
+     * What a process that is gone without keeping its log ended with, as far as is known.
+     */
+    static final String GONE = "the process was gone without stopping Camel (killed, or its JVM crashed)";
+
+    private static final String LOG_SUFFIX = "-previous.log";
+    private static final String INFO_SUFFIX = "-previous.json";
+    private static final Pattern PID_LOG = Pattern.compile("(\\d+)\\.log");
+    private static final int MAX_FAILURE = 500;
+    private static final DateTimeFormatter TIME
+            = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * A run that ended, and the log it kept.
+     *
+     * @param name      the name of the integration
+     * @param pid       the pid it ran as, 0 when not known
+     * @param directory the directory it ran in, or <tt>null</tt>
+     * @param started   when it started, in epoch millis, 0 when not known
+     * @param ended     when it ended, in epoch millis
+     * @param exitCode  the exit code, or <tt>null</tt> when not known (such as of a process that was killed)
+     * @param failure   why it ended, when it failed (such as the error it failed to start with), or <tt>null</tt>
+     * @param log       the log it kept
+     */
+    public record Info(String name, long pid, String directory, long started, long ended, Integer exitCode,
+            String failure, Path log) {
+
+        /**
+         * Which run it was, and how it ended, in one line.
+         */
+        public String describe() {
+            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(endedAt());
+            long ago = System.currentTimeMillis() - ended;
+            if (ago >= 0) {
+                sb.append(" (").append(TimeUtils.printDuration(ago, false)).append(" ago)");
+            }
+            if (exitCode != null) {
+                sb.append(" with exit code ").append(exitCode);
+            }
+            if (failure != null) {
+                sb.append(": ").append(failure);
+            }
+            return sb.toString();
+        }
+    }
+
+    private PreviousRun() {
+    }
+
+    /**
+     * Keeps the log of a run of this JVM that ended as the previous log of its name.
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, String failure) {
+        long started = ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+        return keep(log, new Info(
+                name, pid, System.getProperty("user.dir"), started, System.currentTimeMillis(),
+                exitCode, failure, null));
+    }
+
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, in place of the one kept before.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    static Path keep(Path log, Info info) {
+        if (log == null || info.name() == null || info.name().isBlank() || !Files.isRegularFile(log)) {
+            return null;
+        }
+        Path dir = CommandLineHelper.getCamelDir();
+        String file = fileName(info.name());
+        Path target = dir.resolve(file + LOG_SUFFIX);
+        try {
+            if (Files.size(log) > MAX_SIZE || !move(log, target)) {
+                copyEnd(log, target, MAX_SIZE);
+                try {
+                    Files.deleteIfExists(log);
+                } catch (IOException e) {
+                    // still open (Windows): removed when the process is gone (see keepOrphans)
+                }
+            }
+            JsonObject jo = new JsonObject();
+            jo.put("name", info.name());
+            if (info.pid() > 0) {
+                jo.put("pid", info.pid());
+            }
+            if (info.directory() != null) {
+                jo.put("directory", info.directory());
+            }
+            if (info.started() > 0) {
+                jo.put("started", info.started());
+            }
+            jo.put("ended", info.ended());
+            if (info.exitCode() != null) {
+                jo.put("exitCode", info.exitCode());
+            }
+            if (info.failure() != null) {
+                String failure = info.failure().strip();
+                jo.put("failure", failure.length() > MAX_FAILURE ? failure.substring(0, MAX_FAILURE) + "..." : failure);
+            }
+            Files.writeString(dir.resolve(file + INFO_SUFFIX), jo.toJson());

Review Comment:
   The `.info` of the run before is deleted first, before the log is moved (see the comment at the start of the `try`). A crash between the move and the write then leaves a log without an `.info`, never a log with the `.info` of another run. The previous runs are found by their `.info` files, so such a log is not listed, and the next keep replaces it.
   
   _Claude Code on behalf of Claus Ibsen_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java:
##########
@@ -3052,26 +3054,63 @@ protected int runCustomCamelVersion(KameletMain main) throws Exception {
 
     protected int runKameletMain(KameletMain main) throws Exception {
         Recording jfrRecording = startJfrRecording();
+        PreviousRun.Keeper previousLog = null;
+        if (logFile != null && keepsPreviousLog()) {
+            // CAMEL-25784: the log is kept as the previous log of the integration when the run ends, and so are the
+            // logs that runs which were killed left behind
+            PreviousRun.keepOrphans();
+            previousLog = new PreviousRun.Keeper(logFile, () -> runName(main));
+        }
+        Integer exitCode = null;
+        String failure = null;
         try {
             main.start();
             if (hasNoRoutes(main)) {
                 // it would run doing nothing: a file without routes, or a mistake in it
-                printer().printErr(files.isEmpty()
-                        ? "No routes found" : "No routes found in: " + String.join(", ", files));
+                failure = files.isEmpty() ? "No routes found" : "No routes found in: " + String.join(", ", files);
+                printer().printErr(failure);
                 main.stop();
-                return CliExitCode.FAILURE;
+                exitCode = CliExitCode.FAILURE;
+                return exitCode;
             }
             main.run();
+            int code = main.getExitCode();
+            // no exit code is set when Camel is stopped from outside (such as camel stop)
+            exitCode = code == Integer.MIN_VALUE ? CliExitCode.OK : code;
+            return exitCode;
+        } catch (Exception | Error e) {

Review Comment:
   6339cd749c68 says why in the comment: an `Error`, such as a `NoClassDefFoundError` of a missing dependency, is also how a run can end. It is only noted as why the run ended, so its kept log says so, and it is thrown again.
   
   _Claude Code on behalf of Claus Ibsen_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #28108:
URL: https://github.com/apache/camel/pull/28108


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25784: camel-jbang - the AI tools wait for Camel to start, and read the log of the previous run [camel]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #28108:
URL: https://github.com/apache/camel/pull/28108#issuecomment-6268362033

   <!-- ci-tested-modules -->
   :test_tube: **CI tested the following changed modules:**
   
   - `docs`
   - `dsl/camel-jbang/camel-jbang-core`
   - `dsl/camel-jbang/camel-jbang-mcp`
   
   ---
   
   <details><summary>:microscope: Scalpel shadow comparison — Scalpel: 5 of 706 tested, 8 compile-only — current: 5 all tested</summary>
   
   [Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **5 affected modules** (current approach: 5).
   
   **Skip-tests mode would test 5 modules** (3 direct + 4 downstream), **skip tests for 8** (generated code, meta-modules)
   
   <details><summary>Modules Scalpel would test (5)</summary>
   
   - `camel-jbang-mcp` ← dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java, dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
   - `camel-jbang-plugin-mcp` ← downstream of org.apache.camel:camel-jbang-core
   - `camel-jbang-plugin-tui` ← downstream of org.apache.camel:camel-jbang-core
   - `camel-jbang-plugin-validate` ← downstream of org.apache.camel:camel-jbang-core
   - `camel-launcher-container` ← downstream of org.apache.camel:camel-launcher
   
   </details>
   
   <details><summary>Modules with tests skipped (8)</summary>
   
   - `camel-jbang-it`
   - `camel-jbang-main`
   - `camel-jbang-plugin-edit`
   - `camel-jbang-plugin-generate`
   - `camel-jbang-plugin-kubernetes`
   - `camel-jbang-plugin-test`
   - `camel-launcher`
   - `coverage`
   
   </details>
   
   > :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
   
   </details>
   
   :warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
   
   - `dsl/camel-jbang/camel-jbang-core`: 2 test(s) disabled on GitHub Actions
   - `dsl/camel-jbang/camel-jbang-mcp`: 2 test(s) disabled on GitHub Actions
   
   :bulb: **Manual integration tests recommended:**
   
   > You modified `dsl/camel-jbang/camel-jbang-core`. The related integration tests in `dsl/camel-jbang/camel-jbang-it` are excluded from CI. Consider running them manually:
   > ```
   > mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
   > ```
   
   <details><summary>All tested modules (15 modules, 3m 15s total)</summary>
   
   **Total reactor time:** 3m 15s
   
   | Module | Duration | Status |
   | --- | --- | --- |
   | Camel :: Launcher | 53.4s | SUCCESS |
   | Camel :: JBang :: MCP | 48.6s | SUCCESS |
   | Camel :: JBang :: Plugin :: Validate | 28.5s | SUCCESS |
   | Camel :: Docs | 22.2s | SUCCESS |
   | Camel :: JBang :: Plugin :: Testing | 16.8s | SUCCESS |
   | Camel :: JBang :: Plugin :: Kubernetes | 16.3s | SUCCESS |
   | Camel :: JBang :: Plugin :: Generate | 1.6s | SUCCESS |
   | Camel :: JBang :: Plugin :: MCP | 1.5s | SUCCESS |
   | Camel :: Coverage | 1.5s | SUCCESS |
   | Camel :: JBang :: Plugin :: Edit | 1.4s | SUCCESS |
   | Camel :: JBang :: Integration tests | 1.0s | SUCCESS |
   | Camel :: JBang :: Main | 1.0s | SUCCESS |
   | Camel :: Launcher :: Container | 0.8s | SUCCESS |
   | Camel :: JBang :: Core | n/a |  |
   | Camel :: JBang :: Plugin :: TUI | n/a |  |
   
   **Top 20 slowest modules:**
   - `Camel :: Launcher` (53.4s)
   - `Camel :: JBang :: MCP` (48.6s)
   - `Camel :: JBang :: Plugin :: Validate` (28.5s)
   - `Camel :: Docs` (22.2s)
   - `Camel :: JBang :: Plugin :: Testing` (16.8s)
   - `Camel :: JBang :: Plugin :: Kubernetes` (16.3s)
   - `Camel :: JBang :: Plugin :: Generate` (1.6s)
   - `Camel :: JBang :: Plugin :: MCP` (1.5s)
   - `Camel :: Coverage` (1.5s)
   - `Camel :: JBang :: Plugin :: Edit` (1.4s)
   - `Camel :: JBang :: Integration tests` (1.0s)
   - `Camel :: JBang :: Main` (1.0s)
   - `Camel :: Launcher :: Container` (0.8s)
   
   </details>
   
   ---
   :gear: [View full build and test results](https://github.com/apache/camel/actions/runs/39244968297)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org