feat: add agent memory support#33
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds durable “agent memory” to RxCode (local SwiftData persistence + on-device embeddings) and exposes it via Settings UI and IDE MCP tools so agents can recall/store long-lived preferences/facts/decisions across sessions. Also includes Live Activity healing/end-of-stall handling between desktop ↔ mobile.
Changes:
- Persist, embed, search, and auto-extract durable memories; inject relevant memories into agent context.
- Add Settings UI for enabling memory, auto-creation, context injection, and browsing/editing/deleting stored memories.
- Expose memory CRUD/search as IDE tools and wire “rxcode-ide” MCP bridge/instructions into Codex/Claude paths.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| RxCodeMobile/State/MobileLiveActivityCoordinator.swift | Reconciles/ends stalled job Live Activities when sessions update or app foregrounds. |
| RxCode/Views/SettingsView.swift | Adds Memory settings section and sheets for browsing/editing memories. |
| RxCode/Services/ThreadStore.swift | Registers MemoryRecord in SwiftData schema and adds CRUD/touch helpers. |
| RxCode/Services/OpenAISummarizationService.swift | Adds prompt + call path to generate JSON “memory operations”. |
| RxCode/Services/MobileSyncService.swift | Ends orphaned aggregate job activity when desktop has no tracked jobs. |
| RxCode/Services/MemoryService.swift | New actor for embedding-backed memory indexing/search + persistence bridge. |
| RxCode/Services/MCPService.swift | Allows optional Codex config override to inject rxcode-ide MCP server. |
| RxCode/Services/IDEServer/AppState+IDEToolHandling.swift | Adds IDE tool handlers for memory search/add/update/delete. |
| RxCode/Services/FoundationModelSummarizationService.swift | Adds memory-ops generation using the Apple foundation model path. |
| RxCode/Services/CodexAppServer.swift | Adds memory-ops generation + conditional IDE-tools developer instructions. |
| RxCode/Services/ClaudeService.swift | Adds memory-ops generation + expands IDE tool system prompt for memory. |
| RxCode/App/AppState.swift | Adds memory toggles, injection into prompts, auto-extraction pipeline, and parsing/applying ops. |
| Packages/Sources/RxCodeCore/Models/MemoryRecord.swift | New SwiftData model + DTOs for durable memory items and vectors. |
| Packages/Sources/RxCodeCore/Backend/IDEToolRegistry.swift | Registers memory tools and their JSON schemas for MCP exposure. |
Comments suppressed due to low confidence (1)
RxCode/App/AppState.swift:5235
- Same prompt-label duplication applies to the ACP path:
memoryContextPromptPrefix(...)adds a "User request:" header, then the skill-context wrapper prefixes another "User request:" whenskillContextexists. Consider consolidating so the final prompt contains the header exactly once.
resolvedPrompt = memoryContextPromptPrefix(for: resolvedMemoryContext, prompt: resolvedPrompt)
if let skillContext = await marketplace.promptContext(for: .acp) {
resolvedPrompt = "\(skillContext)\n\nUser request:\n\(resolvedPrompt)"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+654
to
+660
| func touchMemories(ids: [String], at date: Date = .now) { | ||
| guard !ids.isEmpty else { return } | ||
| for id in ids { | ||
| fetchMemory(id: id)?.touch(at: date) | ||
| } | ||
| save() | ||
| } |
Comment on lines
+64
to
+67
| let hits = Array(ranked) | ||
| if let store = threadStore { | ||
| let ids = hits.map(\.item.id) | ||
| await MainActor.run { store.touchMemories(ids: ids) } |
Comment on lines
+5214
to
5217
| resolvedPrompt = memoryContextPromptPrefix(for: resolvedMemoryContext, prompt: resolvedPrompt) | ||
| if let skillContext = await marketplace.promptContext(for: .codex) { | ||
| resolvedPrompt = "\(skillContext)\n\nUser request:\n\(prompt)" | ||
| resolvedPrompt = "\(skillContext)\n\nUser request:\n\(resolvedPrompt)" | ||
| } |
Comment on lines
+897
to
+907
| HStack { | ||
| Text("Memory") | ||
| .font(.system(size: ClaudeTheme.size(13), weight: .semibold)) | ||
| Spacer() | ||
| Button { | ||
| editingDraft = MemoryDraft() | ||
| } label: { | ||
| Label("Add", systemImage: "plus") | ||
| } | ||
| .help("Add memory") | ||
| Button { |
Comment on lines
+1226
to
+1233
| Button("Save") { | ||
| Task { | ||
| await performSave() | ||
| dismiss() | ||
| } | ||
| } | ||
| .keyboardShortcut(.defaultAction) | ||
| .disabled(draft.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
Comment on lines
+3575
to
+3612
| private static func parseMemoryOperations(_ raw: String) -> [MemoryOperation] { | ||
| let trimmed = stripJSONFence(raw) | ||
| guard let range = jsonArrayRange(in: trimmed) else { return [] } | ||
| let json = String(trimmed[range]) | ||
| guard let data = json.data(using: .utf8), | ||
| let array = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] | ||
| else { return [] } | ||
| return array.compactMap { entry in | ||
| guard let action = entry["action"] as? String else { return nil } | ||
| return MemoryOperation( | ||
| action: action.lowercased(), | ||
| id: entry["id"] as? String, | ||
| content: entry["content"] as? String, | ||
| kind: entry["kind"] as? String, | ||
| scope: entry["scope"] as? String | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private static func stripJSONFence(_ raw: String) -> String { | ||
| var text = raw.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| if text.hasPrefix("```") { | ||
| var lines = text.components(separatedBy: "\n") | ||
| if !lines.isEmpty { lines.removeFirst() } | ||
| if lines.last?.trimmingCharacters(in: .whitespacesAndNewlines) == "```" { | ||
| lines.removeLast() | ||
| } | ||
| text = lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) | ||
| } | ||
| return text | ||
| } | ||
|
|
||
| private static func jsonArrayRange(in text: String) -> Range<String.Index>? { | ||
| guard let start = text.firstIndex(of: "["), | ||
| let end = text.lastIndex(of: "]"), | ||
| start <= end else { return nil } | ||
| return start..<text.index(after: end) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.