Skip to content

Comments

feat(web): memory management in web ui#127

Open
skulldogged wants to merge 3 commits intospacedriveapp:mainfrom
skulldogged:web-memory-management
Open

feat(web): memory management in web ui#127
skulldogged wants to merge 3 commits intospacedriveapp:mainfrom
skulldogged:web-memory-management

Conversation

@skulldogged
Copy link
Contributor

Summary

  • Add full memory management in the web UI: users can create, edit, and remove memories directly from the Memories view.
  • Extend the API to support memory CRUD operations on /api/agents/memories (POST/PUT/DELETE), including embedding sync for create/update/delete so search stays consistent.
  • Improve memory editing UX by replacing the importance field with a slider plus synced numeric input for precise control.

Details

  • Backend
    • Added handlers in src/api/memories.rs for create, update, and soft-delete (forget) flows.
    • Wired new routes in src/api/server.rs under /agents/memories.
    • On create/update, embeddings are generated/stored; on content update, old embedding is replaced; on forget, embedding is removed.
  • Frontend API client
    • Added request/response types and methods for memory create/update/delete in interface/src/api/client.ts.
  • Web UI
    • Updated interface/src/routes/AgentMemories.tsx with:
      • "Add memory" modal
      • Edit and remove actions in expanded memory rows
      • Importance control using slider + manual number input (0-1, step 0.01)
      • Query invalidation to refresh list/search after mutations

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

This pull request adds complete CRUD (Create, Update, Delete) capabilities for agent memories across the full stack: frontend API client with typed request/response interfaces, a comprehensive React UI with forms and dialogs for managing memories, backend handlers with validation and embedding management, and updated routing to support the new operations alongside existing list and search endpoints.

Changes

Cohort / File(s) Summary
Memory API Client
interface/src/api/client.ts
Introduces 4 new interfaces (MemoryCreateRequest, MemoryUpdateRequest, MemoryWriteResponse, MemoryDeleteResponse) and 3 API methods (createMemory, updateMemory, deleteMemory) for memory CRUD operations with standard fetch-based HTTP calls.
Memory UI Component
interface/src/routes/AgentMemories.tsx
Adds comprehensive memory management UI with Create/Edit/Delete forms, mutation-driven state management, dialog-based editors, form validation (content required, importance 0-1), query client integration for data refresh, and expanded actions for each memory item.
Memory Backend Handlers
src/api/memories.rs
Implements create, update, and delete handlers with request/response structures; includes validation, embedding creation/updates/deletion, FTS index management, importance clamping, timestamp updates, and forgotten state tracking; extends MemoriesListResponse with total count.
API Route Configuration
src/api/server.rs
Expands /agents/memories route from single GET handler to multi-method endpoint supporting POST (create), PUT (update), DELETE (delete), and GET (list).

Poem

🐰 Memories now dance in the digital warren,
Create, update, delete—oh what marvelous bargain!
With embeddings tucked and indexes so fine,
The agent remembers what matters in time.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% 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
Title check ✅ Passed The title 'feat(web): memory management in web ui' clearly and concisely summarizes the main change—adding full memory CRUD operations to the web UI—which is the primary objective of this pull request.
Description check ✅ Passed The description is directly related to the changeset, providing a well-structured overview of backend API extensions, frontend client additions, and UI improvements for memory management across all modified files.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
interface/src/routes/AgentMemories.tsx (2)

272-274: virtualizer in the dependency array may be unnecessary.

useVirtualizer returns a stable instance (ref-backed), so including it won't cause an infinite loop, but the effect is really driven by memories.length and expandedId. Adding virtualizer just satisfies the exhaustive-deps lint — which is fine, but worth knowing it's inert.

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

In `@interface/src/routes/AgentMemories.tsx` around lines 272 - 274, The effect
calls virtualizer.measure() but the dependency array only needs to react to
changes in memories.length and expandedId; virtualizer is a stable ref from
useVirtualizer and can be removed from the dependencies to avoid a noisy
lint-driven inclusion. Update the useEffect in AgentMemories.tsx to depend only
on [memories.length, expandedId] (or if you must keep the lint happy, remove
virtualizer and add a single-line eslint-disable comment above the useEffect for
react-hooks/exhaustive-deps), leaving the call to virtualizer.measure()
unchanged.

177-260: Consider passing form data as the mutate argument instead of closing over component state.

createMutation and updateMutation read formData and editingMemory from the component closure rather than receiving them as mutate(…) arguments. This works today because saveMemory validates and calls mutate() synchronously in the same render, but it's fragile if the flow ever becomes async (e.g., adding a confirmation step).

♻️ Example: passing form data through mutate
 const createMutation = useMutation({
-    mutationFn: () => {
-      return api.createMemory({
-        agent_id: agentId,
-        content: formData.content,
-        memory_type: formData.memory_type,
-        importance: formData.importance,
-      });
-    },
+    mutationFn: (data: MemoryFormData) => {
+      return api.createMemory({
+        agent_id: agentId,
+        content: data.content,
+        memory_type: data.memory_type,
+        importance: data.importance,
+      });
+    },
     ...
 });

 const updateMutation = useMutation({
-    mutationFn: () => {
-      if (!editingMemory) throw new Error("No memory selected");
-      return api.updateMemory({
-        agent_id: agentId,
-        memory_id: editingMemory.id,
-        content: formData.content,
-        memory_type: formData.memory_type,
-        importance: formData.importance,
-      });
-    },
+    mutationFn: ({ memoryId, data }: { memoryId: string; data: MemoryFormData }) => {
+      return api.updateMemory({
+        agent_id: agentId,
+        memory_id: memoryId,
+        content: data.content,
+        memory_type: data.memory_type,
+        importance: data.importance,
+      });
+    },
     ...
 });

Then in saveMemory:

 if (editingMemory) {
-    updateMutation.mutate();
+    updateMutation.mutate({ memoryId: editingMemory.id, data: formData });
 } else {
-    createMutation.mutate();
+    createMutation.mutate(formData);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/AgentMemories.tsx` around lines 177 - 260,
createMutation and updateMutation close over component state (formData,
editingMemory) which is fragile; modify both mutations so their mutationFn
accepts an input argument (e.g., variables) and uses that to call
api.createMemory / api.updateMemory instead of reading formData/editingMemory
from closure, and then change saveMemory to build a plain payload (trimmed
content, memory_type, importance, and for updates include editingMemory.id) and
call createMutation.mutate(payload) or updateMutation.mutate(payload); keep the
same success/error handlers (refreshMemories, setEditorOpen, setFormError, etc.)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/memories.rs`:
- Around line 195-226: The handler currently calls store.save(&memory) before
creating/storing the embedding, which leaves a memory persisted without an
embedding if embed_one or embedding_table().store fails; either move the
embedding step before persisting (call
embedding_model_arc().embed_one(&memory.content) first, then store.save(&memory)
and embedding_table().store) or, if persisting first is required, add a rollback
on failure (on any error from embed_one or embedding_table().store call
store.delete(&memory.id) or equivalent to remove the partially saved memory),
and keep the existing ensure_fts_index() call after successful storage; update
the code paths around store.save, embedding_model_arc().embed_one,
embedding_table().store, and error handling to implement one of these fixes.

---

Nitpick comments:
In `@interface/src/routes/AgentMemories.tsx`:
- Around line 272-274: The effect calls virtualizer.measure() but the dependency
array only needs to react to changes in memories.length and expandedId;
virtualizer is a stable ref from useVirtualizer and can be removed from the
dependencies to avoid a noisy lint-driven inclusion. Update the useEffect in
AgentMemories.tsx to depend only on [memories.length, expandedId] (or if you
must keep the lint happy, remove virtualizer and add a single-line
eslint-disable comment above the useEffect for react-hooks/exhaustive-deps),
leaving the call to virtualizer.measure() unchanged.
- Around line 177-260: createMutation and updateMutation close over component
state (formData, editingMemory) which is fragile; modify both mutations so their
mutationFn accepts an input argument (e.g., variables) and uses that to call
api.createMemory / api.updateMemory instead of reading formData/editingMemory
from closure, and then change saveMemory to build a plain payload (trimmed
content, memory_type, importance, and for updates include editingMemory.id) and
call createMutation.mutate(payload) or updateMutation.mutate(payload); keep the
same success/error handlers (refreshMemories, setEditorOpen, setFormError, etc.)
unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7170ca0 and eeadc3a.

📒 Files selected for processing (4)
  • interface/src/api/client.ts
  • interface/src/routes/AgentMemories.tsx
  • src/api/memories.rs
  • src/api/server.rs

Comment on lines +195 to +226
store.save(&memory).await.map_err(|error| {
tracing::warn!(%error, agent_id = %request.agent_id, "failed to create memory");
StatusCode::INTERNAL_SERVER_ERROR
})?;

let embedding = memory_search
.embedding_model_arc()
.embed_one(&memory.content)
.await
.map_err(|error| {
tracing::warn!(%error, agent_id = %request.agent_id, memory_id = %memory.id, "failed to embed memory content");
StatusCode::INTERNAL_SERVER_ERROR
})?;

memory_search
.embedding_table()
.store(&memory.id, &memory.content, &embedding)
.await
.map_err(|error| {
tracing::warn!(%error, agent_id = %request.agent_id, memory_id = %memory.id, "failed to store memory embedding");
StatusCode::INTERNAL_SERVER_ERROR
})?;

if let Err(error) = memory_search.embedding_table().ensure_fts_index().await {
tracing::warn!(%error, "failed to ensure FTS index after memory create");
}

Ok(Json(MemoryWriteResponse {
success: true,
memory,
}))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Memory is persisted before embedding — partial state on embedding failure.

If store.save() succeeds (Line 195) but embedding generation (Line 200) or storage (Line 209) fails, the handler returns a 500 but the memory already exists in the store without an embedding. This memory will appear in list views but be invisible to semantic search.

Consider either: (a) generating the embedding before saving the memory, or (b) cleaning up the saved memory on embedding failure.

🛡️ Option (a): embed first, then persist
+    // Generate embedding before persisting so we don't create orphaned memories
+    let embedding = memory_search
+        .embedding_model_arc()
+        .embed_one(&content)
+        .await
+        .map_err(|error| {
+            tracing::warn!(%error, agent_id = %request.agent_id, "failed to embed memory content");
+            StatusCode::INTERNAL_SERVER_ERROR
+        })?;
+
     store.save(&memory).await.map_err(|error| {
         tracing::warn!(%error, agent_id = %request.agent_id, "failed to create memory");
         StatusCode::INTERNAL_SERVER_ERROR
     })?;

-    let embedding = memory_search
-        .embedding_model_arc()
-        .embed_one(&memory.content)
-        .await
-        .map_err(|error| {
-            tracing::warn!(%error, agent_id = %request.agent_id, memory_id = %memory.id, "failed to embed memory content");
-            StatusCode::INTERNAL_SERVER_ERROR
-        })?;
-
     memory_search
         .embedding_table()
         .store(&memory.id, &memory.content, &embedding)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/memories.rs` around lines 195 - 226, The handler currently calls
store.save(&memory) before creating/storing the embedding, which leaves a memory
persisted without an embedding if embed_one or embedding_table().store fails;
either move the embedding step before persisting (call
embedding_model_arc().embed_one(&memory.content) first, then store.save(&memory)
and embedding_table().store) or, if persisting first is required, add a rollback
on failure (on any error from embed_one or embedding_table().store call
store.delete(&memory.id) or equivalent to remove the partially saved memory),
and keep the existing ensure_fts_index() call after successful storage; update
the code paths around store.save, embedding_model_arc().embed_one,
embedding_table().store, and error handling to implement one of these fixes.

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