Skip to content

feat(memory): workspace-first retrieval priority in AutoMemoryStore#352

Merged
xinhuagu merged 2 commits into
mainfrom
feat/351-memory-retrieval-scope
Mar 28, 2026
Merged

feat(memory): workspace-first retrieval priority in AutoMemoryStore#352
xinhuagu merged 2 commits into
mainfrom
feat/351-memory-retrieval-scope

Conversation

@xinhuagu

@xinhuagu xinhuagu commented Mar 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • query(), search(), and formatForPrompt() now return workspace entries before global entries
  • Workspace entries fill the limit first; global entries only appear as fallback
  • Implementation: workspaceFirstMerge() partitions by scope using existing entryFiles tracking

Why

AutoMemoryStore loads both workspace and global entries into one list. Previously, query/search treated them equally — global entries could compete with workspace entries for limited prompt slots. Now workspace-specific memories always take priority for project-specific work.

Test plan

  • queryReturnsWorkspaceEntriesBeforeGlobal
  • queryLimitPrefersWorkspaceEntries — limit 3 with 3+3 entries → only workspace
  • queryFillsGlobalWhenWorkspaceInsufficient — workspace not enough → global fills
  • queryWorksWithOnlyGlobalEntries — no workspace → global still works
  • searchReturnsWorkspaceEntriesBeforeGlobal
  • formatForPromptPrefersWorkspaceEntries — limit 1 → only workspace entry
  • All existing AutoMemoryStoreTest tests still pass

Closes #351

Summary by CodeRabbit

  • New Features

    • Query and search now prioritize workspace entries before global entries and apply result limits after this workspace-first ordering.
  • Bug Fixes

    • Adjusted ranking/limiting so limits reflect workspace-first combined results (avoids dropping workspace items when filling with global items).
  • Tests

    • Added tests to validate workspace-first ordering and limit behavior.
  • Documentation

    • Updated docs to explain workspace-first prioritization and limit semantics.

query(), search(), and formatForPrompt() now return workspace-scoped
entries before global entries. Workspace entries fill the budget first;
global entries only appear as fallback when workspace results are
insufficient. This ensures project-specific memories take priority
over cross-project global memories during prompt injection.

Implementation: workspaceFirstMerge() partitions results by scope
using the existing entryFiles tracking, then concatenates workspace
entries before global entries within the limit.

Closes #351

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The change implements workspace-first retrieval in AutoMemoryStore: query and search now classify entries by scope, merge workspace results before global ones while preserving relative order, and apply limits after merging. Added helpers for scope detection and merging, plus tests covering ordering and limit behavior.

Changes

Cohort / File(s) Summary
Workspace-first retrieval implementation
aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java
Modified query(...) and search(...) to always sort by recency, partition ranked results into workspace vs global using isGlobalEntry(...), merge with workspace-first ordering via workspaceFirstMerge(...), and apply limit after merging. Updated Javadocs and added inline comments describing unbounded-then-merge ranking.
Workspace-first behavior tests
aceclaw-memory/src/test/java/dev/aceclaw/memory/AutoMemoryStoreTest.java
Added six JUnit tests validating workspace-first ordering and post-merge limit semantics for query(...), search(...), and formatForPrompt(...) (including cases where limit is smaller than workspace results and when workspace results are fewer than limit).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Block Major Correctness And Security Risks ❌ Error Concurrent read/write access to unsynchronized HashMap<String, String> entryFiles causes data race; reads in query()/search() lack locking while writes hold accessLock. Use ConcurrentHashMap for entryFiles or acquire accessLock before calling workspaceFirstMerge(); add concurrency tests for query()/search() with concurrent writes.
✅ Passed checks (7 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: implementing workspace-first retrieval priority in AutoMemoryStore, which is the core feature of this PR.
Linked Issues check ✅ Passed All coding requirements from issue #351 are met: workspace-scoped entries are now preferred in retrieval paths, global entries serve as fallback, scope-aware implementation via workspaceFirstMerge() is in place, and Javadocs are updated to document the behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #351 requirements: modifications to query(), search(), and formatForPrompt() implement workspace-first prioritization, new helper methods support scope classification, and tests validate the scoped retrieval behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Require Test Coverage For New Logic ✅ Passed The PR includes comprehensive test coverage for the new business logic with six new test methods validating the new functionality and modifications to existing methods.
No Api Breaking Changes Without Version Bump ✅ Passed PR claims no public API signature changes; modifications are internal implementation only.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/351-memory-retrieval-scope

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 1

🧹 Nitpick comments (1)
aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java (1)

648-655: Consider returning a defensive copy from subList.

subList() returns a view backed by the original list. While this works correctly in the current usage, returning a copy would provide more consistent behavior for callers.

♻️ Suggested improvement
         if (limit > 0 && merged.size() > limit) {
-            return merged.subList(0, limit);
+            return new ArrayList<>(merged.subList(0, limit));
         }
-        return merged;
+        return List.copyOf(merged);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java` around
lines 648 - 655, The method in AutoMemoryStore currently returns
merged.subList(0, limit) which is a view into the backing list; change it to
return a defensive copy instead: replace the subList return with new
ArrayList<MemoryEntry>(merged.subList(0, limit)) and also consider returning new
ArrayList<MemoryEntry>(merged) in the non-truncated path so callers always get
an independent list (use the existing merged variable for both changes).
🤖 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-memory/src/test/java/dev/aceclaw/memory/AutoMemoryStoreTest.java`:
- Around line 565-576: In the test method
searchReturnsWorkspaceEntriesBeforeGlobal, tighten the result-size check by
replacing the weak assertion assertThat(results).hasSizeGreaterThanOrEqualTo(2)
with an exact assertion assertThat(results).hasSize(2) because you add exactly
two entries via store.add (MemoryEntry.Category.PATTERN) and expect only those
two from store.search; update the assertion so the test fails if unexpected
additional entries are returned.

---

Nitpick comments:
In `@aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java`:
- Around line 648-655: The method in AutoMemoryStore currently returns
merged.subList(0, limit) which is a view into the backing list; change it to
return a defensive copy instead: replace the subList return with new
ArrayList<MemoryEntry>(merged.subList(0, limit)) and also consider returning new
ArrayList<MemoryEntry>(merged) in the non-truncated path so callers always get
an independent list (use the existing merged variable for both changes).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 52de9f5d-1547-4f71-8a73-783008aaabb0

📥 Commits

Reviewing files that changed from the base of the PR and between 9b78935 and 5811be5.

📒 Files selected for processing (2)
  • aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java
  • aceclaw-memory/src/test/java/dev/aceclaw/memory/AutoMemoryStoreTest.java

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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: 1

🧹 Nitpick comments (1)
aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java (1)

636-655: Inconsistent mutability of returned list.

When limit > 0 && merged.size() > limit, the method returns an immutable List.copyOf(). Otherwise, it returns the mutable ArrayList directly. This inconsistency could cause subtle bugs if callers assume immutability.

Additionally, per coding guidelines, consider null-guarding the ranked parameter.

Proposed fix
 private List<MemoryEntry> workspaceFirstMerge(List<MemoryEntry> ranked, int limit) {
+    var safe = ranked != null ? ranked : List.<MemoryEntry>of();
     var workspace = new ArrayList<MemoryEntry>();
     var global = new ArrayList<MemoryEntry>();

-    for (var entry : ranked) {
+    for (var entry : safe) {
         if (isGlobalEntry(entry)) {
             global.add(entry);
         } else {
             workspace.add(entry);
         }
     }

     var merged = new ArrayList<MemoryEntry>(workspace);
     merged.addAll(global);

     if (limit > 0 && merged.size() > limit) {
         return List.copyOf(merged.subList(0, limit));
     }
-    return merged;
+    return List.copyOf(merged);
 }

As per coding guidelines: "Guard against null collection parameters before calling methods like .stream() — use pattern: var safe = list != null ? list : List.of()"

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

In `@aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java` around
lines 636 - 655, The method workspaceFirstMerge currently returns a mutable
ArrayList in one path and an immutable List.copyOf in another and does not guard
ranked for null; update workspaceFirstMerge to null-guard the ranked parameter
(e.g., var safe = ranked != null ? ranked : List.of()), build the merged list
from safe, and always return an immutable list (use List.copyOf(merged) or
List.copyOf(merged.subList(0, limit)) when limit applies) so callers get a
consistent immutable result while preserving the existing workspace/global
ordering and limit behavior.
🤖 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-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java`:
- Around line 620-626: The isGlobalEntry method reads the non-thread-safe
entryFiles map without synchronization, causing a data race against writers
(add, remove, replaceEntries); to fix, either change entryFiles to a thread-safe
ConcurrentHashMap so reads in isGlobalEntry are safe, or ensure callers acquire
the same accessLock used by mutators (e.g., have query() and search() acquire
accessLock before calling workspaceFirstMerge which calls isGlobalEntry) so all
access to entryFiles is protected; update the constructor or field declaration
if switching to ConcurrentHashMap, or add lock acquisitions around
workspaceFirstMerge/query/search to use accessLock consistently.

---

Nitpick comments:
In `@aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java`:
- Around line 636-655: The method workspaceFirstMerge currently returns a
mutable ArrayList in one path and an immutable List.copyOf in another and does
not guard ranked for null; update workspaceFirstMerge to null-guard the ranked
parameter (e.g., var safe = ranked != null ? ranked : List.of()), build the
merged list from safe, and always return an immutable list (use
List.copyOf(merged) or List.copyOf(merged.subList(0, limit)) when limit applies)
so callers get a consistent immutable result while preserving the existing
workspace/global ordering and limit behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c679aa6-2f4b-4eab-b3a5-fa2a26f0588a

📥 Commits

Reviewing files that changed from the base of the PR and between 5811be5 and 92aeb58.

📒 Files selected for processing (2)
  • aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java
  • aceclaw-memory/src/test/java/dev/aceclaw/memory/AutoMemoryStoreTest.java
✅ Files skipped from review due to trivial changes (1)
  • aceclaw-memory/src/test/java/dev/aceclaw/memory/AutoMemoryStoreTest.java

Comment on lines +620 to +626
/**
* Returns whether an entry belongs to the global scope (vs workspace-scoped).
*/
boolean isGlobalEntry(MemoryEntry entry) {
String file = entryFiles.get(entry.id());
return GLOBAL_FILE.equals(file);
}

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 | 🔴 Critical

Data race on entryFiles HashMap.

This method reads from entryFiles without synchronization, but write operations (add, remove, replaceEntries, etc.) modify it under locks. Since HashMap is not thread-safe, concurrent read/write can cause undefined behavior or ConcurrentModificationException.

Consider either:

  1. Using ConcurrentHashMap for entryFiles, or
  2. Acquiring accessLock in query()/search() before calling workspaceFirstMerge().
Option 1: Use ConcurrentHashMap
-    private final Map<String, String> entryFiles;
+    private final Map<String, String> entryFiles = new ConcurrentHashMap<>();

And in constructor:

-        this.entryFiles = new HashMap<>();
+        // entryFiles already initialized as ConcurrentHashMap
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-memory/src/main/java/dev/aceclaw/memory/AutoMemoryStore.java` around
lines 620 - 626, The isGlobalEntry method reads the non-thread-safe entryFiles
map without synchronization, causing a data race against writers (add, remove,
replaceEntries); to fix, either change entryFiles to a thread-safe
ConcurrentHashMap so reads in isGlobalEntry are safe, or ensure callers acquire
the same accessLock used by mutators (e.g., have query() and search() acquire
accessLock before calling workspaceFirstMerge which calls isGlobalEntry) so all
access to entryFiles is protected; update the constructor or field declaration
if switching to ConcurrentHashMap, or add lock acquisitions around
workspaceFirstMerge/query/search to use accessLock consistently.

@xinhuagu xinhuagu merged commit d23c5af into main Mar 28, 2026
4 checks passed
@xinhuagu xinhuagu deleted the feat/351-memory-retrieval-scope branch March 28, 2026 18:22
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.

feat(memory): make auto-memory retrieval prefer workspace scope over global fallback

1 participant