Skip to content

Fix memory expiration logic and Makefile warnings#6

Open
fakebizprez wants to merge 2 commits intomainfrom
fix-memory-expiration-7162277752654284590
Open

Fix memory expiration logic and Makefile warnings#6
fakebizprez wants to merge 2 commits intomainfrom
fix-memory-expiration-7162277752654284590

Conversation

@fakebizprez
Copy link
Copy Markdown
Member

@fakebizprez fakebizprez commented Feb 17, 2026

  • Fix SQL queries in src/plugin/internal/db/libsql.go to correctly compare expires_at (RFC3339) with datetime('now') by wrapping the column in datetime().
  • Fix duplicate target warnings in src/plugin/Makefile by removing directory rules and using mkdir -p in recipes.
  • Add regression test TestExpirationBehavior in src/plugin/internal/db/libsql_test.go to verify expiration logic.

PR created automatically by Jules for task 7162277752654284590 started by @fakebizprez

Summary by CodeRabbit

  • Bug Fixes
    • Improved database expiration handling to ensure expired entries are accurately identified and removed.

@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

The pull request modifies build configuration, database expiration handling, and adds expiration behavior testing. Build targets now create output directories inline with mkdir -p instead of separate prerequisites. Database queries explicitly convert expires_at to datetime before comparison. A new test validates expiration logic with TTL manipulation and cleanup.

Changes

Cohort / File(s) Summary
Build Configuration
src/plugin/Makefile
Refactors directory creation by moving mkdir -p calls inline within targets (build, build-mcp, test-coverage, dist) instead of using separate target prerequisites for BUILD_DIR and DIST_DIR.
Database Expiration Logic
src/plugin/internal/db/libsql.go
Updates SQL queries across multiple operations (cleanup, retrieval, listing, searching, counting) to wrap expires_at with datetime() function in comparisons: datetime(expires_at) > datetime('now') instead of direct comparison.
Database Tests
src/plugin/internal/db/libsql_test.go
Adds TestExpirationBehavior test that validates expired record retrieval, manual database verification, cleanup execution, and record removal. Includes time package import for TTL manipulation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Build directories now bloom inline,
Datetime conversions help times align,
Tests prove expiration works just right,
Our rabbit code now shines so bright! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: fixing memory expiration logic and Makefile warnings. It directly reflects the primary changes across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)

✅ Unit Test PR creation complete.

  • Create PR with unit tests
  • Commit unit tests in branch fix-memory-expiration-7162277752654284590
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 17, 2026

Required label not found on this PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/plugin/internal/db/libsql_test.go (1)

466-467: Prefer errors.Is for error comparison.

Using errors.Is(err, ErrNotFound) is more idiomatic and resilient to future wrapping of the error. The same pattern exists at line 146.

Suggested fix
-	if err != ErrNotFound {
-		t.Errorf("expected ErrNotFound for expired memory, got: %v", err)
+	if !errors.Is(err, ErrNotFound) {
+		t.Errorf("expected ErrNotFound for expired memory, got: %v", err)

This requires adding "errors" to the import block.

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

In `@src/plugin/internal/db/libsql_test.go` around lines 466 - 467, Replace direct
equality checks against ErrNotFound with errors.Is for robust error comparison:
add "errors" to the import block, and change the test assertions that currently
do `if err != ErrNotFound { t.Errorf(...) }` to use `if !errors.Is(err,
ErrNotFound) { t.Errorf(...) }` for the occurrences around the current check and
the earlier similar check near line 146 (both referencing ErrNotFound and
calling t.Errorf).
src/plugin/internal/db/libsql.go (1)

865-865: ListNamespaces does not filter out expired memories.

ListNamespaces still returns namespaces that may only contain expired (but not yet cleaned-up) memories. This is pre-existing behavior, but worth noting for consistency with the other queries that now properly exclude expired entries.

Suggested fix
-	rows, err := d.db.QueryContext(ctx, `SELECT DISTINCT namespace FROM memories ORDER BY namespace`)
+	rows, err := d.db.QueryContext(ctx, `SELECT DISTINCT namespace FROM memories WHERE (expires_at IS NULL OR datetime(expires_at) > datetime('now')) ORDER BY namespace`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/internal/db/libsql.go` at line 865, ListNamespaces currently
selects DISTINCT namespace from memories without excluding expired rows; update
the SQL in the ListNamespaces function to only return namespaces that have at
least one non-expired memory by adding a WHERE clause like "WHERE expiration IS
NULL OR expiration > CURRENT_TIMESTAMP" (or "WHERE expiration IS NULL OR
expiration > ?" and pass time.Now().UTC() as the parameter) so expired-only
namespaces are filtered out; modify the d.db.QueryContext call in ListNamespaces
to use the new query and provide the current time parameter if you choose the
parameterized form.
src/plugin/Makefile (1)

128-136: Consider extracting a helper variable or splitting the dist target.

The dist target body is fairly long (8 recipe lines). You could define a list of platform tuples and loop over them to reduce duplication, though this is purely cosmetic.

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

In `@src/plugin/Makefile` around lines 128 - 136, The dist target is repetitive;
define a helper variable (e.g., PLATFORMS = darwin/amd64 darwin/arm64
linux/amd64 linux/arm64 windows/amd64) and replace the eight explicit build
lines in the dist recipe with a loop (using make's foreach or a shell for) that
iterates PLATFORMS, splits each tuple to GOOS/GOARCH, and invokes $(GO) build
with $(LDFLAGS) to output to $(DIST_DIR)/$(BINARY_NAME)-<os>-<arch>[.exe];
alternatively create a per-platform helper target (e.g., build-platform) that
accepts GOOS/GOARCH and have dist call it for each tuple, keeping references to
DIST_DIR, BINARY_NAME, GO and LDFLAGS unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/plugin/internal/db/libsql_test.go`:
- Around line 466-467: Replace direct equality checks against ErrNotFound with
errors.Is for robust error comparison: add "errors" to the import block, and
change the test assertions that currently do `if err != ErrNotFound {
t.Errorf(...) }` to use `if !errors.Is(err, ErrNotFound) { t.Errorf(...) }` for
the occurrences around the current check and the earlier similar check near line
146 (both referencing ErrNotFound and calling t.Errorf).

In `@src/plugin/internal/db/libsql.go`:
- Line 865: ListNamespaces currently selects DISTINCT namespace from memories
without excluding expired rows; update the SQL in the ListNamespaces function to
only return namespaces that have at least one non-expired memory by adding a
WHERE clause like "WHERE expiration IS NULL OR expiration > CURRENT_TIMESTAMP"
(or "WHERE expiration IS NULL OR expiration > ?" and pass time.Now().UTC() as
the parameter) so expired-only namespaces are filtered out; modify the
d.db.QueryContext call in ListNamespaces to use the new query and provide the
current time parameter if you choose the parameterized form.

In `@src/plugin/Makefile`:
- Around line 128-136: The dist target is repetitive; define a helper variable
(e.g., PLATFORMS = darwin/amd64 darwin/arm64 linux/amd64 linux/arm64
windows/amd64) and replace the eight explicit build lines in the dist recipe
with a loop (using make's foreach or a shell for) that iterates PLATFORMS,
splits each tuple to GOOS/GOARCH, and invokes $(GO) build with $(LDFLAGS) to
output to $(DIST_DIR)/$(BINARY_NAME)-<os>-<arch>[.exe]; alternatively create a
per-platform helper target (e.g., build-platform) that accepts GOOS/GOARCH and
have dist call it for each tuple, keeping references to DIST_DIR, BINARY_NAME,
GO and LDFLAGS unchanged.

Copy link
Copy Markdown

@llamapreview llamapreview bot left a comment

Choose a reason for hiding this comment

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

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Request Changes

This PR fixes a critical bug in memory expiration logic that prevented correct cleanup and retrieval but introduces potential performance degradation and maintainability concerns that should be addressed.

📄 Documentation Diagram

This diagram documents the fixed memory expiration cleanup workflow.

sequenceDiagram
    participant User
    participant Scheduler as Cleanup Scheduler
    participant DB as Database
    User->>Scheduler: Start periodic cleanup
    Scheduler->>DB: Call cleanupExpired()
    DB->>DB: Execute DELETE query with datetime(expires_at)
    note over DB: PR #35;6 fixed datetime() wrapping for correct expiration comparison
    DB-->>Scheduler: Return success/error
    Scheduler-->>User: Cleanup completed
Loading

🌟 Strengths

  • Addresses a critical system-wide bug ensuring expired memories are properly handled.
  • Adds a new regression test to validate the expiration behavior.
Priority File Category Impact Summary Anchors
P1 src/plugin/internal/db/libsql.go Bug Fixes expiration comparison bug affecting all memories system-wide. method:cleanupExpired
P2 src/plugin/internal/db/libsql.go Performance datetime() wrapping may degrade query performance by preventing index usage. method:Retrieve
P2 src/plugin/internal/db/libsql.go Architecture Creates coupling to SQLite datetime() function, risking maintainability. -
P2 src/plugin/internal/db/libsql_test.go Testing Missing test coverage for boundary conditions and concurrent cleanup. -
P2 src/plugin/Makefile Maintainability Introduces redundant mkdir -p commands across multiple targets. -

🔍 Notable Themes

  • Expiration Logic Overhaul: The fix corrects a critical bug but trades off query performance and introduces architectural coupling to SQLite's datetime() function.
  • Build System Cleanup: Makefile changes eliminate duplicate target warnings but add minor redundancy that could be centralized for better maintainability.

📈 Risk Diagram

This diagram illustrates the critical expiration comparison bug and its fix.

sequenceDiagram
    participant Memory as Memory Entry
    participant Check as Expiration Check
    participant Cleanup as Cleanup Process
    Memory->>Check: Check if expired (with bug)
    Check->>Check: Compare expires_at string lexicographically
    note over Check: R1(P1): Incorrect comparison leads to wrong cleanup
    Check-->>Memory: May be incorrectly kept or deleted
    Memory->>Check: Check if expired (fixed)
    Check->>Check: Compare datetime(expires_at) with datetime('now')
    note over Check: Fixed in PR #35;6: Proper RFC3339 comparison
    Check-->>Memory: Correctly identified as expired or not
Loading
⚠️ **Unanchored Suggestions (Manual Review Recommended)**

The following suggestions could not be precisely anchored to a specific line in the diff. This can happen if the code is outside the changed lines, has been significantly refactored, or if the suggestion is a general observation. Please review them carefully in the context of the full file.


📁 File: src/plugin/internal/db/libsql_test.go

The new regression test correctly validates the fixed expiration behavior. However, it only tests the negative TTL case. Missing test coverage for: 1) exact boundary condition (expires_at = now), 2) future expiration that should be returned, and 3) interaction with cleanupExpired when called concurrently with retrievals. The test also directly queries the database (db.db.QueryRowContext), which couples it to internal implementation.

Suggestion:

// Test exact expiration boundary
mem.TTL = &zeroTTL // time.Duration(0)
if err := db.Store(ctx, mem); err != nil { t.Fatalf("store failed: %v", err) }
// Should be treated as expired immediately
_, err = db.Retrieve(ctx, "default", mem.Key)
if err != ErrNotFound { t.Errorf("expected ErrNotFound for expired memory, got: %v", err) }

Related Code:

func TestExpirationBehavior(t *testing.T) {
    // Store a memory that has already expired
    negTTL := -1 * time.Hour
    mem := &Memory{
        ID:        "expired-mem",
        Namespace: "default",
        Key:       "expired-key",
        Value:     "This should be expired",
        TTL:       &negTTL,
        Embedding: make([]float32, 384),
    }

    if err := db.Store(ctx, mem); err != nil {
        t.Fatalf("store failed: %v", err)
    }

    // Try to Retrieve it. It should NOT be returned.
    _, err = db.Retrieve(ctx, "default", "expired-key")
    if err != ErrNotFound {
        t.Errorf("expected ErrNotFound for expired memory, got: %v", err)
    }
}


💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

Comment on lines 321 to 323
_, err := d.db.ExecContext(ctx,
`DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < datetime('now')`,
`DELETE FROM memories WHERE expires_at IS NOT NULL AND datetime(expires_at) < datetime('now')`,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 | Confidence: High

The change wraps expires_at in datetime() for proper RFC3339 string comparison. This fixes a critical logic bug where expiration comparisons were previously performing lexicographic string comparisons, causing incorrect cleanup and retrieval behavior. The cleanupExpired method is called periodically by StartCleanup() (found in related context), meaning this bug affected automatic expiration of all memories system-wide. Without this fix, memories could remain active past their expiration or be prematurely deleted.

Comment on lines 822 to 826
var count int64
err := d.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM memories WHERE namespace = ? AND (expires_at IS NULL OR expires_at > datetime('now'))`,
`SELECT COUNT(*) FROM memories WHERE namespace = ? AND (expires_at IS NULL OR datetime(expires_at) > datetime('now'))`,
namespace,
).Scan(&count)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

The Count and CountAll methods now use the same datetime() wrapping pattern, ensuring consistency. However, this creates a subtle architectural coupling: all expiration logic now depends on SQLite's datetime() function behavior with RFC3339 strings. If the storage format or database engine changes, all eight query locations must be updated in sync. This is a maintainability risk, though mitigated by the new regression test.

Comment thread src/plugin/Makefile
Comment on lines +47 to 50
build:
@mkdir -p $(BUILD_DIR)
@echo "Building $(BINARY_NAME) $(VERSION)..."
$(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

The Makefile changes correctly eliminate duplicate target warnings by replacing directory rule dependencies with explicit mkdir -p in recipes. This simplifies the Makefile and follows best practices. However, it introduces minor redundancy—the mkdir -p command appears in five targets (build, build-mcp, test-coverage, dist, and implicitly in any recipe writing to those directories). This is acceptable but could be centralized using a macro or variable if more directories are added.

Code Suggestion:

define ensure_dir
    @mkdir -p $(1)
endef

build:
    $(call ensure_dir,$(BUILD_DIR))
    @echo "Building $(BINARY_NAME) $(VERSION)..."
    $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/

Comment on lines 588 to 593
SELECT id, namespace, key, value, embedding, metadata, tags, created_at, updated_at, ttl, expires_at
FROM memories
WHERE namespace = ?
AND (expires_at IS NULL OR expires_at > datetime('now'))
AND (expires_at IS NULL OR datetime(expires_at) > datetime('now'))
ORDER BY created_at DESC
LIMIT ? OFFSET ?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: Medium

Speculative: Applying datetime() to the expires_at column in WHERE clauses prevents any existing index on expires_at from being used efficiently. While necessary for correctness, this could degrade query performance as the table grows, especially for List, Search, and Count operations which may scan large datasets. The related context shows Retrieve is called via store.Retrieve in the memory layer, indicating these queries are on the critical path for user operations.

Code Suggestion:

CREATE INDEX idx_memories_expires_at_datetime ON memories(datetime(expires_at));

Evidence: method:Retrieve

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

✅ Created PR with unit tests: #7

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