personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove legacy backward compatibility code and redundant data

Clean up three areas of legacy/backward compatibility:

1. Entity API: Remove 'desc' key fallback, use only 'description'
2. App facets config: Remove bool support, use dict-only format
3. Chat architecture: Remove redundant chat_id/thread storage

Chat refactor details:
- chat_id is now derived from filename (was redundant)
- thread is now derived on-demand via get_agent_thread()
- Removed normalize_chat() function and all call sites
- find_chat_by_agent() now uses get_agent_thread() for O(n) lookup
instead of O(n*m) search through all chat threads

This eliminates data duplication, removes stale data risks, and
simplifies the codebase by removing all normalization/fallback logic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+83 -118
+9 -19
apps/__init__.py
··· 26 26 { 27 27 "icon": "🏠", # Emoji icon for menu bar (default: "📦") 28 28 "label": "Custom Label", # Display label (default: title-cased app name) 29 - "facets": true, # Enable facet integration (default: true) 29 + "facets": {}, # Facet options: {"disabled": true} to hide, {"muted": true} to show disabled facets 30 30 "date_nav": true, # Show date navigation bar (default: false) 31 31 "allow_future_dates": true # Allow future dates in month picker (default: false) 32 32 } ··· 67 67 background_template: Optional[str] = None 68 68 69 69 # Facet configuration (optional, default {}) 70 - # Can be bool (backwards compat) or dict with options: 70 + # Options: 71 + # - disabled: If true, facets bar is hidden for this app 71 72 # - muted: Include facets marked as disabled in facet.json 72 - facets_config: bool | dict = field(default_factory=dict) 73 + facets_config: dict = field(default_factory=dict) 73 74 74 75 # Date navigation (renders date nav below facet bar) 75 76 date_nav: bool = False ··· 79 80 80 81 def facets_enabled(self) -> bool: 81 82 """Check if facets are enabled for this app.""" 82 - if isinstance(self.facets_config, bool): 83 - return self.facets_config 84 - if isinstance(self.facets_config, dict): 85 - return not self.facets_config.get("disabled", False) 86 - return True 83 + return not self.facets_config.get("disabled", False) 87 84 88 85 def show_muted_facets(self) -> bool: 89 86 """Check if muted/disabled facets should be shown.""" 90 - if isinstance(self.facets_config, dict): 91 - return self.facets_config.get("muted", False) 92 - return False 87 + return self.facets_config.get("muted", False) 93 88 94 89 def date_nav_enabled(self) -> bool: 95 90 """Check if date nav is enabled for this app.""" ··· 174 169 icon = metadata.get("icon", "📦") 175 170 label = metadata.get("label", app_name.replace("_", " ").title()) 176 171 177 - # Parse facets config: can be bool or dict 178 - facets_raw = metadata.get("facets", {}) 179 - if isinstance(facets_raw, bool): 180 - # Backwards compat: true means enabled, false means disabled 181 - facets_config = facets_raw 182 - elif isinstance(facets_raw, dict): 183 - facets_config = facets_raw 184 - else: 172 + # Parse facets config 173 + facets_config = metadata.get("facets", {}) 174 + if not isinstance(facets_config, dict): 185 175 facets_config = {} 186 176 187 177 # Date navigation
+1 -1
apps/chat/background.html
··· 10 10 {% if recent_chats %} 11 11 {% for chat in recent_chats %} 12 12 { 13 - chat_id: '{{ chat.chat_id or chat.agent_id }}', 13 + chat_id: '{{ chat.chat_id }}', 14 14 title: {{ chat.title|default('(untitled)')|tojson }}, 15 15 ts: {{ chat.ts|default(0) }}, 16 16 unread: {{ 'true' if chat.unread else 'false' }},
+65 -83
apps/chat/routes.py
··· 15 15 logger = logging.getLogger(__name__) 16 16 17 17 18 - def normalize_chat(chat_data: dict, fallback_id: str | None = None) -> dict: 19 - """Normalize a chat record to ensure required fields exist. 20 - 21 - Handles legacy records that may be missing chat_id or thread. 18 + def _load_chat(chat_id: str) -> dict | None: 19 + """Load a chat record by ID, injecting chat_id from filename. 22 20 23 21 Args: 24 - chat_data: Raw chat record dict 25 - fallback_id: ID to use if chat_id/agent_id missing (e.g., filename stem) 22 + chat_id: The chat ID (filename stem) 26 23 27 24 Returns: 28 - Normalized chat dict with chat_id and thread guaranteed 25 + Chat data dict with chat_id injected, or None if not found 29 26 """ 30 - # Ensure chat_id exists 31 - if "chat_id" not in chat_data: 32 - chat_data["chat_id"] = chat_data.get("agent_id", fallback_id) 27 + chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 28 + chat_file = chats_dir / f"{chat_id}.json" 33 29 34 - # Ensure thread exists 35 - if "thread" not in chat_data: 36 - legacy_id = chat_data.get("agent_id", chat_data.get("chat_id")) 37 - if legacy_id: 38 - chat_data["thread"] = [legacy_id] 30 + if not chat_file.exists(): 31 + return None 39 32 33 + chat_data = load_json(chat_file) 34 + if chat_data: 35 + chat_data["chat_id"] = chat_id 40 36 return chat_data 41 37 42 38 ··· 48 44 49 45 50 46 def _load_all_chats() -> tuple[list[dict], int]: 51 - """Load and normalize all chat records. 47 + """Load all chat records. 52 48 53 49 Returns: 54 - Tuple of (chats list, unread count). Each chat dict has chat_id normalized. 50 + Tuple of (chats list, unread count). Each chat dict has chat_id injected. 55 51 """ 56 52 chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 57 53 chats = [] ··· 61 57 for chat_file in chats_dir.glob("*.json"): 62 58 chat_data = load_json(chat_file) 63 59 if chat_data: 64 - normalize_chat(chat_data, chat_file.stem) 60 + chat_data["chat_id"] = chat_file.stem 65 61 chats.append(chat_data) 66 62 if chat_data.get("unread"): 67 63 unread_count += 1 ··· 129 125 resp.status_code = 400 130 126 return resp 131 127 132 - # For continuation, we need to find the last agent in the thread 128 + # For continuation, derive thread to find last agent 133 129 config: dict[str, Any] = {} 134 - chats_dir = get_app_storage_path("chat", "chats") 135 - existing_chat = None 130 + chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 131 + is_continuation = False 136 132 137 - if continue_chat: 138 - chat_file = chats_dir / f"{continue_chat}.json" 139 - if chat_file.exists(): 140 - existing_chat = load_json(chat_file) 141 - if existing_chat: 142 - normalize_chat(existing_chat, continue_chat) 143 - # Continue from the last agent in the thread 144 - last_agent = existing_chat["thread"][-1] 145 - config["continue_from"] = last_agent 133 + if continue_chat and (chats_dir / f"{continue_chat}.json").exists(): 134 + from muse.cortex_client import get_agent_thread 135 + 136 + # Derive thread from chat_id (which equals first agent_id) 137 + try: 138 + thread = get_agent_thread(continue_chat) 139 + config["continue_from"] = thread[-1] 140 + is_continuation = True 141 + except FileNotFoundError: 142 + pass # Chat exists but agent file missing - treat as new 146 143 147 144 if backend == "openai": 148 145 key_name = "OPENAI_API_KEY" ··· 182 179 183 180 ts = int(time.time() * 1000) 184 181 185 - if existing_chat: 186 - # Continuation: append new agent to existing chat's thread 187 - existing_chat["thread"].append(agent_id) 188 - existing_chat["updated_ts"] = ts 189 - chat_file = chats_dir / f"{continue_chat}.json" 190 - save_json(chat_file, existing_chat) 182 + if is_continuation: 183 + # Continuation: update timestamp only (thread is derived from agents) 184 + chat_data = load_json(chats_dir / f"{continue_chat}.json") 185 + if chat_data: 186 + chat_data["updated_ts"] = ts 187 + save_json(chats_dir / f"{continue_chat}.json", chat_data) 191 188 chat_id = continue_chat 192 189 else: 193 - # New chat: create record with thread array 190 + # New chat: create metadata record (no thread stored) 194 191 chat_id = agent_id 195 192 title = generate_chat_title(message) 196 193 chat_record = { 197 - "chat_id": chat_id, 198 - "thread": [agent_id], 199 194 "ts": ts, 200 195 "facet": facet, 201 196 "title": title, 202 197 } 203 - chat_file = chats_dir / f"{chat_id}.json" 204 - save_json(chat_file, chat_record) 198 + # Ensure chats directory exists for new chats 199 + chats_dir = get_app_storage_path("chat", "chats") 200 + save_json(chats_dir / f"{chat_id}.json", chat_record) 205 201 206 202 return jsonify(chat_id=chat_id, agent_id=agent_id) 207 203 except Exception as e: ··· 225 221 def chat_events(chat_id: str) -> Any: 226 222 """Return all events from a chat thread. 227 223 228 - Loads the chat record, then hydrates events from all agents in the thread. 224 + Derives thread from agent files, then hydrates events from all agents. 229 225 For active chats, client should subscribe to WebSocket for real-time updates. 230 226 """ 231 - from muse.cortex_client import get_agent_status, read_agent_events 227 + from muse.cortex_client import get_agent_status, get_agent_thread, read_agent_events 232 228 233 - chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 234 - chat_file = chats_dir / f"{chat_id}.json" 235 - 236 - if not chat_file.exists(): 229 + chat = _load_chat(chat_id) 230 + if not chat: 237 231 resp = jsonify({"error": f"Chat not found: {chat_id}"}) 238 232 resp.status_code = 404 239 233 return resp 240 234 241 - chat = load_json(chat_file) 242 - if not chat: 243 - resp = jsonify({"error": f"Failed to load chat: {chat_id}"}) 244 - resp.status_code = 500 245 - return resp 246 - 247 - normalize_chat(chat, chat_id) 248 - thread = chat["thread"] 235 + # Derive thread from agent files (chat_id = first agent_id) 236 + try: 237 + thread = get_agent_thread(chat_id) 238 + except FileNotFoundError: 239 + thread = [chat_id] # Fallback for very new agents 249 240 250 241 # Hydrate events from all agents in the thread 251 242 all_events = [] ··· 290 281 Returns: 291 282 Chat metadata JSON or 404 if not found 292 283 """ 293 - chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 294 - chat_file = chats_dir / f"{chat_id}.json" 295 - 296 - if not chat_file.exists(): 284 + chat_data = _load_chat(chat_id) 285 + if not chat_data: 297 286 resp = jsonify({"error": f"Chat not found: {chat_id}"}) 298 287 resp.status_code = 404 299 288 return resp 300 289 301 - chat_data = load_json(chat_file) 302 - if not chat_data: 303 - resp = jsonify({"error": f"Failed to load chat: {chat_id}"}) 304 - resp.status_code = 500 305 - return resp 306 - 307 290 return jsonify(chat_data) 308 291 309 292 ··· 311 294 def find_chat_by_agent(agent_id: str) -> Any: 312 295 """Find the chat that contains a given agent_id in its thread. 313 296 314 - This is used by the background service to find which chat a completion 315 - event belongs to, since continuation agents have different IDs than 316 - the chat itself. 297 + Uses get_agent_thread() to derive the thread from agent files, then 298 + looks up the chat by the root agent ID (which equals chat_id). 317 299 318 300 Args: 319 301 agent_id: The agent ID to search for 320 302 321 303 Returns: 322 - Chat metadata JSON or 404 if not found in any thread 304 + Chat metadata JSON or 404 if not found 323 305 """ 324 - chats_dir = get_app_storage_path("chat", "chats", ensure_exists=False) 306 + from muse.cortex_client import get_agent_thread 325 307 326 - if not chats_dir.exists(): 327 - resp = jsonify({"error": "No chats found"}) 308 + try: 309 + # Derive thread from agent - first element is the root/chat_id 310 + thread = get_agent_thread(agent_id) 311 + chat_id = thread[0] 312 + except FileNotFoundError: 313 + resp = jsonify({"error": f"Agent not found: {agent_id}"}) 328 314 resp.status_code = 404 329 315 return resp 330 316 331 - # Search all chats for one containing this agent_id in its thread 332 - for chat_file in chats_dir.glob("*.json"): 333 - chat_data = load_json(chat_file) 334 - if chat_data: 335 - normalize_chat(chat_data, chat_file.stem) 336 - if agent_id in chat_data.get("thread", []): 337 - return jsonify(chat_data) 317 + chat_data = _load_chat(chat_id) 318 + if not chat_data: 319 + resp = jsonify({"error": f"Chat not found for agent: {agent_id}"}) 320 + resp.status_code = 404 321 + return resp 338 322 339 - resp = jsonify({"error": f"No chat found containing agent: {agent_id}"}) 340 - resp.status_code = 404 341 - return resp 323 + return jsonify(chat_data) 342 324 343 325 344 326 @chat_bp.route("/api/chat/<chat_id>/read", methods=["POST"]) ··· 456 438 # Filter by facet if specified 457 439 if facet is not None and chat_data.get("facet") != facet: 458 440 continue 459 - normalize_chat(chat_data, link.stem) 441 + chat_data["chat_id"] = link.stem 460 442 bookmarks.append(chat_data) 461 443 462 444 # Sort by bookmarked timestamp, newest first
-2
apps/chat/tools.py
··· 73 73 actor, caller_agent_id = _get_actor_info(context) 74 74 75 75 chat_record = { 76 - "chat_id": agent_id, 77 - "thread": [agent_id], 78 76 "ts": int(agent_id), # agent_id is already the timestamp 79 77 "from": {"type": "agent", "id": caller_agent_id or "mcp_tool"}, 80 78 "title": title,
+1 -1
apps/chat/workspace.html
··· 337 337 list.className = 'recent-chats-list'; 338 338 339 339 for (const chat of chats) { 340 - const chatId = chat.chat_id || chat.agent_id; // Support legacy records 340 + const chatId = chat.chat_id; 341 341 const item = document.createElement('a'); 342 342 item.className = 'recent-chat-item'; 343 343 item.href = `#${chatId}`;
+1 -3
apps/entities/routes.py
··· 69 69 70 70 etype = data.get("type", "").strip() 71 71 name = data.get("name", "").strip() 72 - # Support both "desc" and "description" for backwards compatibility 73 - desc = data.get("desc", "") or data.get("description", "") 74 - desc = desc.strip() 72 + desc = data.get("description", "").strip() 75 73 76 74 if not etype or not name: 77 75 return jsonify({"error": "Type and name are required"}), 400
+1 -1
apps/entities/workspace.html
··· 1072 1072 body: JSON.stringify({ 1073 1073 type: entity.type, 1074 1074 name: entity.name, 1075 - desc: entity.desc || '' 1075 + description: entity.desc || '' 1076 1076 }) 1077 1077 }) 1078 1078 .then(response => response.json())
+1 -1
apps/live/app.json
··· 1 1 { 2 2 "icon": "⚡", 3 3 "label": "Live Events", 4 - "facets": false 4 + "facets": {"disabled": true} 5 5 }
+1 -1
apps/speakers/app.json
··· 2 2 "icon": "🎙️", 3 3 "label": "Speakers", 4 4 "date_nav": true, 5 - "facets": true 5 + "facets": {} 6 6 }
+1 -2
fixtures/journal/apps/chat/chats/1764019444672.json
··· 1 1 { 2 - "agent_id": "1764019444672", 3 2 "ts": 1764019444672, 4 3 "from": { 5 4 "type": "agent", ··· 7 6 }, 8 7 "title": "Unified storage test: synthetic agent with chat me...", 9 8 "unread": true 10 - } 9 + }
+1 -2
fixtures/journal/apps/chat/chats/1764019602551.json
··· 1 1 { 2 - "agent_id": "1764019602551", 3 2 "ts": 1764019602551, 4 3 "from": { 5 4 "type": "agent", ··· 7 6 }, 8 7 "title": "Clean separation test - cortex creates agent, chat...", 9 8 "unread": true 10 - } 9 + }
+1 -2
fixtures/journal/apps/chat/chats/1764019955580.json
··· 1 1 { 2 - "agent_id": "1764019955580", 3 2 "ts": 1764019955580, 4 3 "from": { 5 4 "type": "agent", ··· 7 6 }, 8 7 "title": "Verify storage location test", 9 8 "unread": true 10 - } 9 + }