Skip to content

feat(daemon): add PatternDetector for recurring tool sequences and workflows (#15)#21

Merged
xinhuagu merged 2 commits into
mainfrom
feature/15-pattern-detector
Feb 19, 2026
Merged

feat(daemon): add PatternDetector for recurring tool sequences and workflows (#15)#21
xinhuagu merged 2 commits into
mainfrom
feature/15-pattern-detector

Conversation

@xinhuagu

@xinhuagu xinhuagu commented Feb 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • PatternDetector with 4 heuristic detection strategies (no LLM calls):
    • REPEATED_TOOL_SEQUENCE — LCS-based subsequence matching, cross-session via AutoMemoryStore
    • ERROR_CORRECTION — cross-turn error aggregation per tool name
    • USER_PREFERENCE — Jaccard similarity grouping of user corrections (threshold 0.6)
    • WORKFLOW — recurring user prompt detection (threshold 0.5)
  • Helper methods: extractToolSequence, isSubsequenceMatch, jaccardSimilarity

Test plan

  • 16 tests in PatternDetectorTest
  • Tool sequence extraction and subsequence matching
  • Jaccard similarity (identical, partial, none, null/blank)
  • User preference detection from repeated corrections
  • Workflow detection from repeated prompts
  • Error correction aggregation across turns
  • Cross-session sequence detection via AutoMemoryStore
  • No false positives on single-occurrence patterns
  • Null/empty edge cases
  • Full build passes (./gradlew clean build — 64 tasks, BUILD SUCCESSFUL)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added pattern detection that analyzes conversation turns and session history to surface repeated tool sequences, recurring errors, user preferences, and common workflows (including optional cross-session recognition).
  • Tests

    • Added comprehensive unit tests covering detection logic, similarity/grouping behavior, edge cases, and cross-session scenarios.

…rkflows (#15)

4 detection strategies: REPEATED_TOOL_SEQUENCE (LCS-based subsequence
matching with cross-session memory lookup), ERROR_CORRECTION (cross-turn
aggregation), USER_PREFERENCE (Jaccard similarity grouping of corrections),
WORKFLOW (recurring user prompt detection). All heuristic-based, no LLM calls.
@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new PatternDetector class that analyzes turns and session history to detect four pattern types (REPEATED_TOOL_SEQUENCE, ERROR_CORRECTION, USER_PREFERENCE, WORKFLOW), with optional cross-session lookups via AutoMemoryStore, plus comprehensive unit tests covering detection strategies and edge cases.

Changes

Cohort / File(s) Summary
Pattern Detection Implementation
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java
New class implementing four detection strategies: REPEATED_TOOL_SEQUENCE (extracts & matches tool sequences, cross-session lookup), ERROR_CORRECTION (aggregates recurring tool errors), USER_PREFERENCE (groups user corrections via Jaccard), WORKFLOW (groups recurring prompts). Adds helper methods (sequence extraction, subsequence/LCS, Jaccard, error extraction), defensive checks, logging, and two constructors (with/without AutoMemoryStore).
Pattern Detector Tests
aceclaw-daemon/src/test/java/dev/aceclaw/daemon/PatternDetectorTest.java
New test suite validating tool sequence extraction, subsequence logic, Jaccard similarity, all four detection strategies, cross-session memory lookup, evidence/confidence calculations, and edge cases (null/empty inputs).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant PatternDetector
participant SessionHistory
participant ToolMetrics
participant AutoMemoryStore

Client->>PatternDetector: analyze(Turn, SessionHistory, ToolMetrics)
PatternDetector->>SessionHistory: read recent conversation messages
PatternDetector->>ToolMetrics: read current tool usage metrics
alt AutoMemoryStore configured
PatternDetector->>AutoMemoryStore: query for matching cross-session sequences
AutoMemoryStore-->>PatternDetector: past sequence matches / evidence
end
PatternDetector-->>Client: List (detected patterns + confidence/evidence)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing PatternDetector for detecting recurring tool sequences and workflows.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/15-pattern-detector

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java (1)

416-422: contentContainsSequence may produce false positives due to substring matching.

The current implementation checks if each tool name is contained as a substring in the content. This can cause false positives:

  • Tool "read" would match content containing "read_file"
  • Tool "file" would match both "read_file" and "write_file"

Consider using word boundary matching or checking for the exact tool name format used in stored patterns.

Possible stricter matching
 private static boolean contentContainsSequence(String content, List<String> sequence) {
     if (content == null) return false;
     for (var tool : sequence) {
-        if (!content.contains(tool)) return false;
+        // Match tool name with word boundaries
+        if (!content.matches("(?s).*\\b" + java.util.regex.Pattern.quote(tool) + "\\b.*")) {
+            return false;
+        }
     }
     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 416 - 422, contentContainsSequence currently uses substring matching which
yields false positives (e.g., "read" matching "read_file"); change it to perform
exact/token or word-boundary matching for each entry in sequence: either
tokenize content (split by non-alphanumeric/underscore chars) and check for
equality against each tool, or use regex with word boundaries and Pattern.quote
for each tool (e.g., Pattern.compile("\\b" + Pattern.quote(tool) + "\\b")).
Update the method contentContainsSequence to use one of these stricter checks
and ensure it matches the exact tool name format used by stored patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java`:
- Around line 67-85: The analyze method currently declares an unused parameter
toolMetrics; either remove it or start using it: if you intend it to influence
detection, thread toolMetrics into the relevant detectors (e.g., pass it into
detectErrorCorrectionPatterns and/or detectRepeatedToolSequences and update
those method signatures and their callers to consult execution stats), otherwise
remove the toolMetrics parameter from PatternDetector.analyze and update all
callers and any tests to stop passing a ToolMetrics map; ensure compiler errors
are fixed by adjusting method signatures for
detectErrorCorrectionPatterns/detectRepeatedToolSequences when chosen.
- Around line 395-396: The use of the underscore lambda parameter in
errors.computeIfAbsent(toolName, _ -> new ArrayList<>()).add(after) is Java 22+
syntax; change the lambda to use a regular parameter name (e.g., key or k) so it
compiles on Java 21 (replace "_" with a named parameter in the computeIfAbsent
call in PatternDetector.java).
- Around line 359-381: In extractToolSequencesFromHistory, the code clears the
working list current when it finds "tool:" but never populates it, so sequences
is always empty; update the logic in extractToolSequencesFromHistory to parse
and append detected tool names into current when processing Assistant messages
(e.g., extract tokens after "tool:" or read structured tool info from
AgentSession.ConversationMessage.Assistant), only clear current when starting a
new assistant turn after adding a completed sequence to sequences, and keep the
existing check against MIN_SEQUENCE_LENGTH before adding to sequences; reference
the current variable, the sequences list, and MIN_SEQUENCE_LENGTH when making
the fix.

---

Nitpick comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java`:
- Around line 416-422: contentContainsSequence currently uses substring matching
which yields false positives (e.g., "read" matching "read_file"); change it to
perform exact/token or word-boundary matching for each entry in sequence: either
tokenize content (split by non-alphanumeric/underscore chars) and check for
equality against each tool, or use regex with word boundaries and Pattern.quote
for each tool (e.g., Pattern.compile("\\b" + Pattern.quote(tool) + "\\b")).
Update the method contentContainsSequence to use one of these stricter checks
and ensure it matches the exact tool name format used by stored patterns.

Comment on lines +67 to +85
public List<PatternInsight> analyze(
Turn turn,
List<AgentSession.ConversationMessage> sessionHistory,
Map<String, ToolMetrics> toolMetrics) {

var insights = new ArrayList<PatternInsight>();

if (turn != null) {
insights.addAll(detectRepeatedToolSequences(turn, sessionHistory));
insights.addAll(detectErrorCorrectionPatterns(turn, sessionHistory));
}

if (sessionHistory != null && !sessionHistory.isEmpty()) {
insights.addAll(detectUserPreferences(sessionHistory));
insights.addAll(detectWorkflows(sessionHistory));
}

return List.copyOf(insights);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused toolMetrics parameter.

The toolMetrics parameter is declared in the method signature but never used in any of the detection strategies. This suggests either incomplete implementation or dead code.

Consider either:

  1. Removing the parameter if it's not needed
  2. Implementing its usage (e.g., incorporating execution statistics into error correction or performance pattern detection)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 67 - 85, The analyze method currently declares an unused parameter
toolMetrics; either remove it or start using it: if you intend it to influence
detection, thread toolMetrics into the relevant detectors (e.g., pass it into
detectErrorCorrectionPatterns and/or detectRepeatedToolSequences and update
those method signatures and their callers to consult execution stats), otherwise
remove the toolMetrics parameter from PatternDetector.analyze and update all
callers and any tests to stop passing a ToolMetrics map; ensure compiler errors
are fixed by adjusting method signatures for
detectErrorCorrectionPatterns/detectRepeatedToolSequences when chosen.

Comment on lines +359 to +381
private List<List<String>> extractToolSequencesFromHistory(
List<AgentSession.ConversationMessage> history) {
// Group by assistant turns, extract tool names from the text
var sequences = new ArrayList<List<String>>();
var current = new ArrayList<String>();

for (var msg : history) {
if (msg instanceof AgentSession.ConversationMessage.Assistant assistant) {
// Extract tool names mentioned in assistant text (heuristic)
var text = assistant.content();
if (text != null && text.contains("tool:")) {
// Simple extraction — real implementation uses structured data
current.clear();
}
} else if (msg instanceof AgentSession.ConversationMessage.User) {
if (current.size() >= MIN_SEQUENCE_LENGTH) {
sequences.add(List.copyOf(current));
}
current.clear();
}
}
return sequences;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

extractToolSequencesFromHistory always returns empty sequences.

The method clears current at line 371 but never adds anything to it. The only path that could add to sequences (line 375) requires current.size() >= MIN_SEQUENCE_LENGTH, which will never be true since current is always empty.

This makes cross-session detection from session history ineffective. Either this is incomplete implementation, or the intent was different.

Possible fix if structured data is available
 private List<List<String>> extractToolSequencesFromHistory(
         List<AgentSession.ConversationMessage> history) {
-    // Group by assistant turns, extract tool names from the text
     var sequences = new ArrayList<List<String>>();
     var current = new ArrayList<String>();

     for (var msg : history) {
         if (msg instanceof AgentSession.ConversationMessage.Assistant assistant) {
-            // Extract tool names mentioned in assistant text (heuristic)
             var text = assistant.content();
-            if (text != null && text.contains("tool:")) {
-                // Simple extraction — real implementation uses structured data
-                current.clear();
+            if (text != null) {
+                // Heuristic: extract tool names from patterns like "tool: <name>" or "[<name>]"
+                var matcher = java.util.regex.Pattern.compile("tool:\\s*(\\w+)|\\[(\\w+)\\]")
+                        .matcher(text);
+                while (matcher.find()) {
+                    String toolName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
+                    if (toolName != null) current.add(toolName);
+                }
             }
         } else if (msg instanceof AgentSession.ConversationMessage.User) {
             if (current.size() >= MIN_SEQUENCE_LENGTH) {
                 sequences.add(List.copyOf(current));
             }
             current.clear();
         }
     }
+    // Don't forget trailing sequence
+    if (current.size() >= MIN_SEQUENCE_LENGTH) {
+        sequences.add(List.copyOf(current));
+    }
     return sequences;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 359 - 381, In extractToolSequencesFromHistory, the code clears the working
list current when it finds "tool:" but never populates it, so sequences is
always empty; update the logic in extractToolSequencesFromHistory to parse and
append detected tool names into current when processing Assistant messages
(e.g., extract tokens after "tool:" or read structured tool info from
AgentSession.ConversationMessage.Assistant), only clear current when starting a
new assistant turn after adding a completed sequence to sequences, and keep the
existing check against MIN_SEQUENCE_LENGTH before adding to sequences; reference
the current variable, the sequences list, and MIN_SEQUENCE_LENGTH when making
the fix.

Comment on lines +395 to +396
errors.computeIfAbsent(toolName, _ -> new ArrayList<>()).add(after);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same Java version concern: unnamed variable _.

This is another instance of the unnamed variable _ syntax (Java 22+). Apply the same fix if Java 21 compatibility is required.

-                        errors.computeIfAbsent(toolName, _ -> new ArrayList<>()).add(after);
+                        errors.computeIfAbsent(toolName, k -> new ArrayList<>()).add(after);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
errors.computeIfAbsent(toolName, _ -> new ArrayList<>()).add(after);
}
errors.computeIfAbsent(toolName, k -> new ArrayList<>()).add(after);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 395 - 396, The use of the underscore lambda parameter in
errors.computeIfAbsent(toolName, _ -> new ArrayList<>()).add(after) is Java 22+
syntax; change the lambda to use a regular parameter name (e.g., key or k) so it
compiles on Java 21 (replace "_" with a named parameter in the computeIfAbsent
call in PatternDetector.java).

ConversationMessage is text-only with no structured tool data.
Cross-session sequence detection uses AutoMemoryStore instead.
@xinhuagu

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@xinhuagu xinhuagu merged commit 64df2dd into main Feb 19, 2026
2 checks passed
@xinhuagu xinhuagu deleted the feature/15-pattern-detector branch February 19, 2026 22:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java`:
- Around line 105-112: The loop over priorPatterns uses contentContainsSequence
which only tests for presence of tool names and can yield false positives;
change the matching to validate order by using the existing isSubsequenceMatch
helper (or by parsing stored MemoryEntry content into a sequence and performing
an ordered subsequence check) when comparing each entry to currentSeq; update
the block that increments frequency and adds evidence (the code touching
MemoryEntry, currentSeq, frequency, evidence, truncate, and memoryStore.query)
to only count and record matches when isSubsequenceMatch confirms the stored
sequence is an ordered subsequence of currentSeq.
- Around line 143-146: The use of the unnamed lambda parameter `_` in the
computeIfAbsent call inside PatternDetector is not Java 21 compatible; update
the computeIfAbsent call on errorTools to use a named parameter (e.g., key or k)
instead of `_` so the lambda is valid in Java 21—locate the block where
ContentBlock.ToolResult tr is handled (the computeIfAbsent call referencing
findToolName(turn.newMessages(), tr.toolUseId())) and replace the underscore
lambda parameter with a simple named parameter like k -> new ArrayList<>().

---

Duplicate comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java`:
- Around line 358-361: The lambda in PatternDetector using
errors.computeIfAbsent(toolName, _ -> new ArrayList<>()) uses the unnamed
parameter "_" which is not Java 21 compatible; change the lambda to use a real
identifier (e.g., key -> new ArrayList<>()) so replace "_" with a named
parameter in the computeIfAbsent call where toolName is the key and errors is
populated (same pattern as the earlier fix at line 145).
- Around line 67-70: The analyze method in PatternDetector declares a
toolMetrics parameter that is never used; either remove it from
PatternDetector.analyze (and all callers such as where
AgentSession.ConversationMessage lists are passed) or thread it into your
detection flow by updating each strategy/collector so they accept Map<String,
ToolMetrics> (or expose a helper that consumes it) and use metrics to weight or
filter PatternInsight results (e.g., scale confidence by execution time/failure
rates from ToolMetrics). Update the PatternDetector.analyze signature, all
callers, and any Strategy/Detector interfaces (and tests) consistently, and
ensure PatternInsight creation includes the metric-influenced score when you
choose to incorporate it.

Comment on lines +105 to +112
var priorPatterns = memoryStore.query(
MemoryEntry.Category.PATTERN, List.of("tool-sequence"), 0);
for (var entry : priorPatterns) {
if (contentContainsSequence(entry.content(), currentSeq)) {
frequency++;
evidence.add("memory: " + truncate(entry.content(), 100));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential false positives in cross-session sequence matching.

contentContainsSequence only checks that each tool name appears somewhere in the content string, ignoring order. A stored entry with "C -> B -> A" would incorrectly match a current sequence of [A, B, C].

Consider using the existing isSubsequenceMatch helper or storing sequences in a parseable format to validate order.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 105 - 112, The loop over priorPatterns uses contentContainsSequence which
only tests for presence of tool names and can yield false positives; change the
matching to validate order by using the existing isSubsequenceMatch helper (or
by parsing stored MemoryEntry content into a sequence and performing an ordered
subsequence check) when comparing each entry to currentSeq; update the block
that increments frequency and adds evidence (the code touching MemoryEntry,
currentSeq, frequency, evidence, truncate, and memoryStore.query) to only count
and record matches when isSubsequenceMatch confirms the stored sequence is an
ordered subsequence of currentSeq.

Comment on lines +143 to +146
if (block instanceof ContentBlock.ToolResult tr && tr.isError()) {
errorTools.computeIfAbsent(findToolName(turn.newMessages(), tr.toolUseId()),
_ -> new ArrayList<>()).add(tr.content());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Java 21 compatibility: unnamed variable _ is Java 22+ syntax.

The underscore _ as an unnamed lambda parameter was finalized in Java 22 (JEP 456). Per coding guidelines requiring Java 21 API compatibility, use a named parameter.

🔧 Proposed fix
                 if (block instanceof ContentBlock.ToolResult tr && tr.isError()) {
                     errorTools.computeIfAbsent(findToolName(turn.newMessages(), tr.toolUseId()),
-                            _ -> new ArrayList<>()).add(tr.content());
+                            k -> new ArrayList<>()).add(tr.content());
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (block instanceof ContentBlock.ToolResult tr && tr.isError()) {
errorTools.computeIfAbsent(findToolName(turn.newMessages(), tr.toolUseId()),
_ -> new ArrayList<>()).add(tr.content());
}
if (block instanceof ContentBlock.ToolResult tr && tr.isError()) {
errorTools.computeIfAbsent(findToolName(turn.newMessages(), tr.toolUseId()),
k -> new ArrayList<>()).add(tr.content());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/PatternDetector.java` around
lines 143 - 146, The use of the unnamed lambda parameter `_` in the
computeIfAbsent call inside PatternDetector is not Java 21 compatible; update
the computeIfAbsent call on errorTools to use a named parameter (e.g., key or k)
instead of `_` so the lambda is valid in Java 21—locate the block where
ContentBlock.ToolResult tr is handled (the computeIfAbsent call referencing
findToolName(turn.newMessages(), tr.toolUseId())) and replace the underscore
lambda parameter with a simple named parameter like k -> new ArrayList<>().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant