You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/20 07:10:29 UTC

(camel) branch fix/CAMEL-25868 created (now bb2ff24decf7)

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

davsclaus pushed a change to branch fix/CAMEL-25868
in repository https://gitbox.apache.org/repos/asf/camel.git


      at bb2ff24decf7 CAMEL-25868: camel-jbang - AI tools: clearer answers for cut doc pages, old errors, consumed files and wrong status header names

This branch includes the following new commits:

     new bb2ff24decf7 CAMEL-25868: camel-jbang - AI tools: clearer answers for cut doc pages, old errors, consumed files and wrong status header names

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-25868: camel-jbang - AI tools: clearer answers for cut doc pages, old errors, consumed files and wrong status header names

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25868
in repository https://gitbox.apache.org/repos/asf/camel.git

commit bb2ff24decf71b6c22018a3314c04682eb5315ff
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue Oct 20 09:10:16 2026 +0200

    CAMEL-25868: camel-jbang - AI tools: clearer answers for cut doc pages, old errors, consumed files and wrong status header names
    
    - The repeat note no longer says a cut camel_catalog_doc page was answered in full, and names docPage=<section title>.
    - camel_get_errors marks the errors from before the last reload (beforeLastReload), as the errors are kept until restart.
    - The reload outcome of a write keeps the first three ERROR lines after the first five lines, and says how many it left out.
    - camel_get_files counts the files a file consumer moved into .camel folders, so a read data folder is not reported empty.
    - The source check reports CamelHttpStatusCode and similar in any route (REST DSL routes use direct), and a name written
      as an Exchange constant that does not exist, which fails the route when Camel creates it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   9 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 114 ++++++++++++++++++++-
 .../dsl/jbang/core/commands/ai/HeaderChecks.java   |  97 +++++++++++++++++-
 .../dsl/jbang/core/commands/ai/ReloadOutcome.java  |  23 ++++-
 .../jbang/core/commands/ai/RepeatedToolCalls.java  |   4 +-
 .../jbang/core/commands/ai/AuthoringToolsTest.java |  92 +++++++++++++++++
 .../jbang/core/commands/ai/ReloadOutcomeTest.java  |  19 ++++
 .../core/commands/ai/RepeatedToolCallsTest.java    |   2 +
 .../commands/ai/SourceValidatorEndpointTest.java   |  54 ++++++++++
 9 files changed, 400 insertions(+), 14 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 7c0cd31eecbc..ee0f2f80da11 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -377,7 +377,9 @@ project `directory` as an argument, the runtime tools take the integration `name
   files up front (`routeFiles`, `configFiles`) and, when an integration is selected, which file and line each of its
   routes was loaded from, mapped from the runtime's location (a jar entry of an exported project, a classpath or
   file resource) onto the source under `src/main`. With `file`, a path relative to the directory as the list names
-  it, the content of that file.
+  it, the content of that file. The `.camel` folders are not listed, but counted (`consumedFiles`): a file consumer
+  moves each file it reads into `.camel` (unless `noop=true`), so a data folder whose files were all read is not
+  reported as empty.
 
 | `camel_write_file`
 | Writes the complete content of a file in the project directory. YAML and `.properties` content is validated
@@ -441,6 +443,8 @@ project `directory` as an argument, the runtime tools take the integration `name
 
 | `camel_get_errors`
 | The failed exchanges of a running integration: route, exchange, exception with stack trace, body and headers.
+  The errors are kept until the integration restarts, so those that failed before the routes were last reloaded are
+  marked with `beforeLastReload`: they come from an earlier version of the routes.
   For an integration that is not running, it says how its previous run ended: when, the exit code, and the failure.
 
 | `camel_eval_expression`
@@ -490,7 +494,8 @@ stuck doing that until its tool call budget is spent. So the first two identical
 in full, and from the third the answer is a short note instead: that the call was already answered and how many
 times it was asked. The changed answer helps the agent break the loop, and the context is not filled with copies.
 `camel_validate_source` counts the same way when it is given `content` and no `directory`: the same source gets
-the same answer, and the note also says to change the line the error names. Validating a file on disk, or
+the same answer, and the note also says to change the line the error names. For `camel_catalog_doc` the note says
+how to get the sections that a cut page left out: `docPage=<section title>` returns one section. Validating a file on disk, or
 `content` with a `directory` (the checks then read the other files of the project, which a fix can change), is
 always answered in full.
 
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 6b878fa93f25..551e175bb5c2 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
@@ -25,6 +25,9 @@ import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -36,6 +39,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
@@ -118,6 +122,8 @@ public final class AuthoringTools {
                 .param("camelVersion", "string", VERSION_DESC, false)
                 .core(true)
                 .deterministic(true)
+                .repeatHint("When that answer was cut, it named the sections it left out: docPage=<section title> returns"
+                            + " one of them.")
                 .executor((ctx, args) -> {
                     applyVersion(ctx, args);
                     return CatalogDocs.catalogDoc(ctx.catalog(), args.get("name"), args.get("endpoint"),
@@ -372,7 +378,9 @@ public final class AuthoringTools {
                         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.";
+                    return errors != null
+                            ? markBeforeLastReload(unescapeBodies(errors), ctx.pid(), p != null ? p.name() : null).toJson()
+                            : "No errors captured.";
                 }));
 
         registry.accept(tool("camel_eval_expression",
@@ -1845,9 +1853,24 @@ public final class AuthoringTools {
         result.put("configFiles", configFiles);
         result.put("files", files);
         result.put("totalFiles", all.size());
+        // a file consumer moves a file it read into .camel (without noop=true), which the listing skips
+        Map<String, Integer> consumed = consumedFolders(dir);
+        String moved = null;
+        if (!consumed.isEmpty()) {
+            JsonObject folders = new JsonObject();
+            folders.putAll(consumed);
+            result.put("consumedFiles", folders);
+            moved = String.join(", ", consumed.entrySet().stream().map(e -> e.getKey() + " holds " + e.getValue()
+                                                                            + (e.getValue() == 1 ? " file" : " files"))
+                    .toList())
+                    + " that a file consumer moved there after reading (noop=true leaves the files where they are)";
+        }
         if (all.isEmpty()) {
-            result.put("message", "The directory has no files");
-        } else if (all.size() > MAX_FILES) {
+            result.put("message", moved != null ? "The directory has no files, but " + moved : "The directory has no files");
+        } else if (moved != null) {
+            result.put("note", moved);
+        }
+        if (all.size() > MAX_FILES) {
             result.put("message", "Listing the first " + MAX_FILES + " of " + all.size()
                                   + " files; routeFiles and configFiles name every route and configuration file");
         }
@@ -2271,6 +2294,91 @@ public final class AuthoringTools {
         }
     }
 
+    /**
+     * Marks the failed exchanges of an earlier version of the routes: those that failed before the newest reload of the
+     * routes in the log. The errors are kept until the integration restarts, and an agent that read them after a change
+     * took them for errors of its new version, and changed routes that already worked.
+     */
+    static JsonObject markBeforeLastReload(JsonObject errors, long pid, String name) {
+        try {
+            JsonObject log = LogFileReader.read(pid, name, 1, ReloadOutcome.RELOADED, null);
+            if (log.get("lines") instanceof List<?> lines && !lines.isEmpty() && lines.get(0) instanceof JsonObject reload) {
+                return markBefore(errors, reload.getString("time"), LocalDateTime.now());
+            }
+        } catch (Exception e) {
+            // no log to tell: the errors as they are
+        }
+        return errors;
+    }
+
+    private static final long RELOAD_MARGIN_MILLIS = 1000;
+
+    /** Marks the errors that failed before the reload at the given time of day; the log keeps the time of day only. */
+    static JsonObject markBefore(JsonObject errors, String reloadTime, LocalDateTime now) {
+        LocalTime time;
+        try {
+            time = LocalTime.parse(reloadTime);
+        } catch (Exception e) {
+            return errors;
+        }
+        LocalDateTime reload = now.toLocalDate().atTime(time);
+        if (reload.isAfter(now)) {
+            reload = reload.minusDays(1);
+        }
+        // the summary is logged after the new routes started: a margin keeps a route that fails at once unmarked
+        long reloadMillis = reload.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - RELOAD_MARGIN_MILLIS;
+        int before = 0;
+        if (errors.get("errors") instanceof List<?> entries) {
+            for (Object o : entries) {
+                if (o instanceof JsonObject e && e.get("timestamp") instanceof Number ts && ts.longValue() < reloadMillis) {
+                    e.put("beforeLastReload", true);
+                    before++;
+                }
+            }
+        }
+        if (before > 0) {
+            errors.put("note", before + (before == 1 ? " exchange" : " exchanges") + " failed before the routes were last"
+                               + " reloaded (at " + reloadTime + "), so on an earlier version of the routes"
+                               + " (beforeLastReload); the errors are kept until the integration restarts");
+        }
+        return errors;
+    }
+
+    /**
+     * The .camel folders under the directory, with the number of files in each: a file consumer moves a file it read
+     * into .camel (its default, without noop=true), so a folder whose files were all read looked empty.
+     */
+    static Map<String, Integer> consumedFolders(Path dir) {
+        Map<String, Integer> answer = new TreeMap<>();
+        try {
+            Files.walkFileTree(dir, EnumSet.noneOf(FileVisitOption.class), MAX_DEPTH, new SimpleFileVisitor<>() {
+                @Override
+                public FileVisitResult preVisitDirectory(Path d, BasicFileAttributes attrs) throws IOException {
+                    String name = d.getFileName() != null ? d.getFileName().toString() : "";
+                    if (".camel".equals(name) && !d.equals(Path.of(System.getProperty("user.home"), ".camel"))) {
+                        try (var files = Files.list(d)) {
+                            long count = files.filter(Files::isRegularFile).count();
+                            if (count > 0) {
+                                answer.put(dir.relativize(d).toString().replace('\\', '/'), (int) count);
+                            }
+                        }
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    return !d.equals(dir) && (SKIPPED_DIRS.contains(name) || name.startsWith("."))
+                            ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFileFailed(Path f, IOException e) {
+                    return FileVisitResult.CONTINUE;
+                }
+            });
+        } catch (IOException e) {
+            // nothing to add
+        }
+        return answer;
+    }
+
     /**
      * Camel JSON-escapes the body of a message dump (MessageHelper), so a JSON body would reach the AI escaped twice (a
      * body like {"orderId":1} showed as {\\"orderId\\":1}). The body values of the errors are unescaped once, as the
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
index 6fc97eedcc48..47b8bd33f75a 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -26,9 +28,12 @@ import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangeConstantProvider;
 import org.apache.camel.catalog.CamelCatalog;
 
 import static org.apache.camel.dsl.jbang.core.commands.ai.PropertiesChecks.closestName;
+import static org.apache.camel.dsl.jbang.core.commands.ai.PropertiesChecks.closestNames;
 
 /**
  * The Camel* header check of {@link SourceValidator}: a header a component never sets, against the header metadata of
@@ -42,6 +47,15 @@ final class HeaderChecks {
     static final Pattern CAMEL_HEADER_REF_PATTERN = Pattern.compile(
             "(?:\\$\\{headers?\\.|headers\\.|headers\\[['\"]|header\\(['\"]|(name):\\s*['\"]?)(Camel[A-Z][A-Za-z0-9]*(?:\\.[A-Za-z0-9_]+)*)");
 
+    private static final String HTTP_STATUS
+            = "the status code of an HTTP answer is the header CamelHttpResponseCode (Exchange.HTTP_RESPONSE_CODE)";
+
+    /**
+     * A name written as a constant of Exchange, such as name: Exchange.HTTP_RESPONSE_CODE: Camel looks the constant up
+     * when it creates the route, for the name of a setHeader, setProperty, setVariable and of the remove EIPs.
+     */
+    static final Pattern EXCHANGE_CONSTANT_NAME = Pattern.compile("name:\\s*['\"]?Exchange\\.([A-Z][A-Z0-9_]*)");
+
     static final Pattern SCHEME_IN_URI_PATTERN = Pattern.compile("uri:\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*):");
 
     /**
@@ -106,6 +120,17 @@ final class HeaderChecks {
                                                               + " ${date:exchangeCreated:yyyy-MM-dd HH:mm:ss}; ${messageTimestamp} is set only by"
                                                               + " components that stamp their messages."));
 
+    /**
+     * Camel* names that are not headers of Camel, for a header that has another name, with what to write: the status
+     * code of an HTTP answer is CamelHttpResponseCode, which models write as CamelHttpStatusCode. Checked whatever the
+     * components of the file are: the routes of a REST DSL use direct, which has no header metadata, so the other
+     * checks skip them.
+     */
+    static final Map<String, String> WRONG_NAMES = Map.of(
+            "CamelHttpStatusCode", HTTP_STATUS, "CamelHttpStatus", HTTP_STATUS, "CamelHttpCode", HTTP_STATUS,
+            "CamelHttpResponseStatus", HTTP_STATUS, "CamelHttpResponseStatusCode", HTTP_STATUS, "CamelStatusCode",
+            HTTP_STATUS);
+
     static String intentHint(String name) {
         for (var e : INTENT_HINTS) {
             if (e.getKey().matcher(name).find()) {
@@ -139,12 +164,64 @@ final class HeaderChecks {
         return msgs;
     }
 
+    /**
+     * The {@link #WRONG_NAMES} the content uses as header names, and the names written as a constant that Exchange does
+     * not have, which fail the route when Camel creates it; once each, the names go into reported.
+     */
+    static List<String> wrongNames(String content, Set<String> reported) {
+        List<String> msgs = new ArrayList<>();
+        String[] lines = content.split("\n", -1);
+        for (int i = 0; i < lines.length; i++) {
+            Matcher c = EXCHANGE_CONSTANT_NAME.matcher(lines[i]);
+            if (c.find() && isSetOrRemoveName(lines, i)) {
+                String hint = exchangeConstantHint(c.group(1));
+                if (hint != null && reported.add("Exchange." + c.group(1))) {
+                    msgs.add("Line " + (i + 1) + ": " + hint);
+                }
+            }
+            Matcher m = CAMEL_HEADER_REF_PATTERN.matcher(lines[i]);
+            while (m.find()) {
+                if (m.group(1) != null && !isHeaderName(lines, i)) {
+                    continue;
+                }
+                String name = m.group(2);
+                String right = WRONG_NAMES.get(name);
+                if (right != null && reported.add(name)) {
+                    msgs.add("Line " + (i + 1) + ": Camel has no header " + name + ": " + right);
+                }
+            }
+        }
+        return msgs;
+    }
+
+    /**
+     * The message for a name written as Exchange.<constant> when Exchange has no such constant, else null: Camel looks
+     * the constant up, so the name Exchange.HTTP_RESPONSE_CODE of a setHeader sets CamelHttpResponseCode.
+     */
+    static String exchangeConstantHint(String constant) {
+        if (ExchangeConstantProvider.lookup(constant) != null) {
+            return null;
+        }
+        String start = "Exchange has no constant " + constant + ", so the route fails to start (Constant field with name: "
+                       + constant + " not found on Exchange.class): ";
+        if (constant.contains("STATUS") || constant.contains("RESPONSE")) {
+            return start + HTTP_STATUS;
+        }
+        List<String> fields = Arrays.stream(Exchange.class.getFields()).map(Field::getName).toList();
+        List<String> closest = closestNames(constant, fields);
+        return start + (closest.isEmpty()
+                ? "write the name of the header, such as CamelFileName"
+                : "did you mean " + String.join(" or ", closest.stream().map(n -> "Exchange." + n).toList()) + "?");
+    }
+
     public static List<String> validateKnownHeaders(String content, CamelCatalog catalog) {
         List<String> msgs = new ArrayList<>();
         if (content == null) {
             return msgs;
         }
         msgs.addAll(timerCounterComparisons(content));
+        Set<String> wrong = new HashSet<>();
+        msgs.addAll(wrongNames(content, wrong));
         Set<String> known = new LinkedHashSet<>();
         Map<String, String> owner = new LinkedHashMap<>();
         Matcher sm = SCHEME_IN_URI_PATTERN.matcher(content);
@@ -189,7 +266,7 @@ final class HeaderChecks {
                     continue;
                 }
                 String name = m.group(2);
-                if (common.contains(name) || reported.contains(name) || isKnown(name, known)) {
+                if (common.contains(name) || reported.contains(name) || wrong.contains(name) || isKnown(name, known)) {
                     continue;
                 }
                 String propertyOwner = null;
@@ -301,6 +378,20 @@ final class HeaderChecks {
 
     /** Whether the name: on this line belongs to a setHeader or removeHeader step, not a setProperty, a bean... */
     static boolean isHeaderName(String[] lines, int index) {
+        String step = parentStep(lines, index);
+        return step.startsWith("setHeader:") || step.startsWith("removeHeader:");
+    }
+
+    /**
+     * Whether the name: of the line names a header, property or variable, which Camel parses as a constant of Exchange.
+     */
+    static boolean isSetOrRemoveName(String[] lines, int index) {
+        String step = parentStep(lines, index);
+        return step.startsWith("set") || step.startsWith("remove");
+    }
+
+    /** The step the line belongs to, such as setHeader:, or an empty text. */
+    private static String parentStep(String[] lines, int index) {
         int indent = YamlLines.countLeadingSpaces(lines[index]);
         for (int j = index - 1; j >= 0; j--) {
             String line = lines[j];
@@ -312,10 +403,10 @@ final class HeaderChecks {
                 if (step.startsWith("- ")) {
                     step = step.substring(2).trim();
                 }
-                return step.startsWith("setHeader:") || step.startsWith("removeHeader:");
+                return step;
             }
         }
-        return false;
+        return "";
     }
 
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
index f816c0b87369..cb4f1e38919e 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
@@ -178,13 +178,15 @@ public final class ReloadOutcome {
      */
     private static final Pattern ROUTE_LOGGER = Pattern.compile("(^|\\.)(yaml|yml|xml|java|groovy):\\d+$");
     private static final int OUTPUT_LINES = 5;
+    private static final int OUTPUT_ERRORS = 3;
     private static final int OUTPUT_CHARS = 400;
 
     /**
      * What the routes logged after the newest reload, waiting up to the timeout for the first of it: the records of the
-     * routes' log steps, and any WARN or ERROR, newest last, at most five, each cut to 400 characters. A write answered
-     * with the result of the change shows the agent at once what its route now does (CAMEL-25513): an agent left to
-     * call camel_get_log reads it before the reload ran, or not at all.
+     * routes' log steps, and any WARN or ERROR, newest last, each cut to 400 characters: the first five, the first
+     * three ERROR after them, and a line that says how many are left out. A write answered with the result of the
+     * change shows the agent at once what its route now does (CAMEL-25513): an agent left to call camel_get_log reads
+     * it before the reload ran, or not at all.
      */
     public static JsonArray awaitOutput(long pid, String name, long timeoutMillis) {
         StopWatch watch = new StopWatch();
@@ -226,7 +228,20 @@ public final class ReloadOutcome {
         // newest first in the log: the first lines the routes logged after the reload are at the end
         Collections.reverse(lines);
         JsonArray out = new JsonArray();
-        out.addAll(lines.subList(0, Math.min(OUTPUT_LINES, lines.size())));
+        int first = Math.min(OUTPUT_LINES, lines.size());
+        out.addAll(lines.subList(0, first));
+        // an ERROR after the first lines is kept as well: the route went on, and its failure is the news
+        int errors = 0;
+        for (String line : lines.subList(first, lines.size())) {
+            if (line.startsWith("ERROR ") && errors < OUTPUT_ERRORS) {
+                out.add(line);
+                errors++;
+            }
+        }
+        int left = lines.size() - out.size();
+        if (left > 0) {
+            out.add("... " + left + " more line" + (left == 1 ? "" : "s") + ": camel_get_log shows them");
+        }
         return out;
     }
 
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
index 1be5f79d2564..2b1350d075f2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
@@ -77,8 +77,8 @@ public final class RepeatedToolCalls {
         note.put("tool", tool.name());
         note.put("timesAsked", times);
         note.put("note", tool.name() + " was already called with these same arguments " + (times - 1)
-                         + " times in this session and answered in full. Its answer does not change, so it is not sent"
-                         + " again: it is in your earlier tool result."
+                         + " times in this session. Its answer does not change, so it is not sent again: it is in your"
+                         + " earlier tool result."
                          + (tool.repeatHint() != null ? " " + tool.repeatHint() : ""));
         return note;
     }
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 f684fb684dec..2ca880ea05a0 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
@@ -20,6 +20,8 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -146,6 +148,96 @@ class AuthoringToolsTest {
                 Map.of("directory", dir.toString(), "file", "missing.yaml")));
     }
 
+    /** A file consumer moves the files it read into .camel, which the listing skips: the folder looked empty. */
+    @Test
+    void filesAFileConsumerMovedIntoDotCamelAreCounted(@TempDir Path dir) throws IOException {
+        Files.createDirectories(dir.resolve("customers/.camel"));
+        Files.writeString(dir.resolve("customers/.camel/C-482.json"), "{}");
+        Files.writeString(dir.resolve("customers/.camel/C-483.json"), "{}");
+        JsonObject list = call("camel_get_files", new ToolContext(), Map.of("directory", dir.toString()));
+        assertEquals(0, list.getInteger("totalFiles"));
+        JsonObject consumed = list.getMap("consumedFiles");
+        assertEquals(1, consumed.size(), consumed.toJson());
+        assertEquals(2, consumed.getInteger("customers/.camel"));
+        assertEquals("The directory has no files, but customers/.camel holds 2 files that a file consumer moved there"
+                     + " after reading (noop=true leaves the files where they are)",
+                list.getString("message"));
+
+        Files.writeString(dir.resolve("demo.camel.yaml"), VALID_ROUTE);
+        list = call("camel_get_files", new ToolContext(), Map.of("directory", dir.toString()));
+        assertEquals(1, list.getInteger("totalFiles"));
+        assertTrue(list.getString("note").startsWith("customers/.camel holds 2 files"), list.toJson());
+        assertFalse(list.containsKey("message"), list.toJson());
+    }
+
+    @Test
+    void noDotCamelFolderNoNote(@TempDir Path dir) throws IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), VALID_ROUTE);
+        Files.createDirectories(dir.resolve("customers/.camel"));
+        JsonObject list = call("camel_get_files", new ToolContext(), Map.of("directory", dir.toString()));
+        assertFalse(list.containsKey("consumedFiles"), "an empty .camel folder is not named");
+        assertFalse(list.containsKey("note"), list.toJson());
+    }
+
+    /** The errors are kept until the integration restarts: those from before the last reload are marked. */
+    @Test
+    void theErrorsFromBeforeTheLastReloadAreMarked() {
+        LocalDateTime now = LocalDateTime.of(2026, 10, 20, 8, 30);
+        JsonObject errors = errorsAt(now.withHour(8).withMinute(10), LocalDateTime.of(2026, 10, 20, 8, 18, 9, 500_000_000),
+                now.withHour(8).withMinute(20));
+
+        AuthoringTools.markBefore(errors, "08:18:09.731", now);
+
+        List<JsonObject> entries = entries(errors);
+        assertEquals(Boolean.TRUE, entries.get(0).get("beforeLastReload"));
+        assertFalse(entries.get(1).containsKey("beforeLastReload"), "within a second of the reload: maybe the new routes");
+        assertFalse(entries.get(2).containsKey("beforeLastReload"), "after the reload");
+        assertEquals("1 exchange failed before the routes were last reloaded (at 08:18:09.731), so on an earlier version"
+                     + " of the routes (beforeLastReload); the errors are kept until the integration restarts",
+                errors.getString("note"));
+    }
+
+    @Test
+    void aReloadLaterInTheDayThanNowWasYesterday() {
+        LocalDateTime now = LocalDateTime.of(2026, 10, 20, 0, 5);
+        JsonObject errors = errorsAt(LocalDateTime.of(2026, 10, 19, 23, 50), LocalDateTime.of(2026, 10, 20, 0, 1));
+
+        AuthoringTools.markBefore(errors, "23:59:00.000", now);
+
+        List<JsonObject> entries = entries(errors);
+        assertEquals(Boolean.TRUE, entries.get(0).get("beforeLastReload"));
+        assertFalse(entries.get(1).containsKey("beforeLastReload"));
+    }
+
+    @Test
+    void noErrorBeforeTheReloadNoNote() {
+        LocalDateTime now = LocalDateTime.of(2026, 10, 20, 8, 30);
+        JsonObject errors = errorsAt(now.withMinute(20));
+        AuthoringTools.markBefore(errors, "08:18:09.731", now);
+        assertFalse(errors.containsKey("note"));
+        assertFalse(entries(errors).get(0).containsKey("beforeLastReload"));
+        // a time the log did not have leaves the errors as they are
+        AuthoringTools.markBefore(errors, "", now);
+        assertFalse(errors.containsKey("note"));
+    }
+
+    private static JsonObject errorsAt(LocalDateTime... times) {
+        JsonArray list = new JsonArray();
+        for (LocalDateTime t : times) {
+            JsonObject e = new JsonObject();
+            e.put("timestamp", t.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+            list.add(e);
+        }
+        JsonObject errors = new JsonObject();
+        errors.put("errors", list);
+        return errors;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<JsonObject> entries(JsonObject errors) {
+        return (List<JsonObject>) (List<?>) errors.getCollection("errors");
+    }
+
     @Test
     void writeValidatesFirstAndRefusesInvalidContent(@TempDir Path dir) throws IOException {
         Files.writeString(dir.resolve("demo.camel.yaml"), "- route: {}\n");
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
index 82b97f3e4904..adb97d9222fa 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
@@ -137,6 +137,25 @@ class ReloadOutcomeTest {
         assertTrue(output.get(1).toString().startsWith("WARN Rollback file strategy"), output.toJson());
     }
 
+    /** The errors after the first lines are the news: a route that went on and failed later on. */
+    @Test
+    void anErrorAfterTheFirstLinesIsKept() {
+        List<String> lines = new java.util.ArrayList<>(RELOADED);
+        for (int i = 1; i <= 6; i++) {
+            lines.add("2026-09-21 10:00:4" + (i + 1) + ".000  INFO 42 --- [ timer://tick] route1 : Order " + i);
+        }
+        lines.add("2026-09-21 10:00:48.500 ERROR 42 --- [ timer://tick] o.a.c.p.e.DefaultErrorHandler : Failed delivery for"
+                  + " exchangeId: A. Exhausted after delivery attempt: 1 caught: java.lang.IllegalStateException: stock");
+        lines.add("2026-09-21 10:00:49.000  INFO 42 --- [ timer://tick] route1 : Order 7");
+
+        JsonArray output = ReloadOutcome.output(records(BEFORE, lines));
+
+        assertEquals(List.of("Hello again", "Order 1", "Order 2", "Order 3", "Order 4"), List.copyOf(output.subList(0, 5)));
+        assertTrue(output.get(5).toString().startsWith("ERROR Failed delivery for exchangeId: A"), output.toJson());
+        assertEquals("... 3 more lines: camel_get_log shows them", output.get(6));
+        assertEquals(7, output.size(), output.toJson());
+    }
+
     @Test
     void whatWasLoggedBeforeTheNewestReloadIsNotOutput() {
         assertEquals(List.of("Hello Camel"), List.copyOf(ReloadOutcome.output(records(BEFORE))));
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
index 2a07467af1e8..c36c47269446 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
@@ -48,6 +48,8 @@ class RepeatedToolCallsTest {
         assertThat(note.getString("tool")).isEqualTo("camel_catalog_doc");
         assertThat(note.getInteger("timesAsked")).isEqualTo(3);
         assertThat(note.getString("note")).contains("2 times");
+        // a page that was cut was not answered in full: the note says how to get the sections it left out
+        assertThat(note.getString("note")).doesNotContain("in full").contains("docPage=<section title>");
 
         assertThat(calls.repeatOf(catalogDoc, sql).getInteger("timesAsked")).isEqualTo(4);
     }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
index b64b7f94305e..7004d5e25002 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
@@ -516,6 +516,60 @@ class SourceValidatorEndpointTest {
         assertThat(msgs).isEmpty();
     }
 
+    /** The routes of a REST DSL use direct, which has no header metadata: a wrong status header is still reported. */
+    @Test
+    void aWrongHttpStatusHeaderIsReportedInADirectRoute() {
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - rest:
+                    get:
+                      - path: /orders/{id}
+                        to: direct:get-order
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - setHeader:
+                            name: CamelHttpStatusCode
+                            constant: 404
+                        - setHeader:
+                            name: Exchange.HTTP_STATUS
+                            constant: 404
+                        - log: "${header.CamelHttpStatusCode}"
+                """, catalog);
+        assertThat(msgs).hasSize(2);
+        assertThat(msgs.get(0)).startsWith("Line 10:").contains("Camel has no header CamelHttpStatusCode")
+                .contains("the header CamelHttpResponseCode (Exchange.HTTP_RESPONSE_CODE)");
+        assertThat(msgs.get(1)).startsWith("Line 13:").contains("Exchange has no constant HTTP_STATUS")
+                .contains("the route fails to start").contains("the header CamelHttpResponseCode");
+    }
+
+    @Test
+    void aConstantOfExchangeIsTheNameCamelLooksUp() {
+        // Camel looks the constant up when it creates the route (DefaultCamelContextExtension.resolvePropertyPlaceholders)
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: direct:get-order
+                    steps:
+                      - setHeader:
+                          name: Exchange.HTTP_RESPONSE_CODE
+                          constant: 404
+                      - setHeader:
+                          name: CamelHttpResponseCode
+                          constant: 404
+                      - setProperty:
+                          name: Exchange.HTTP_STATUS
+                          constant: 404
+                      - setHeader:
+                          name: Exchange.FILE_NAM
+                          constant: out.json
+                """, catalog);
+        // the name of a setProperty is looked up as well
+        assertThat(msgs).hasSize(2);
+        assertThat(msgs.get(0)).startsWith("Line 11:").contains("Exchange has no constant HTTP_STATUS");
+        assertThat(msgs.get(1)).startsWith("Line 14:").contains("Exchange has no constant FILE_NAM")
+                .contains("did you mean Exchange.FILE_NAME");
+    }
+
     @Test
     void withIncludeMetadataAHeaderLookupFindsTheExchangeProperty() {
         // Simple's ${header.x} falls back to the exchange properties, so it reads the counter