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 <[email protected]>
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) <[email protected]>
    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

Reply via email to