personal memory agent
0
fork

Configure Feed

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

Block agent re-attachment of user-detached entities

When a user intentionally removes (detaches) an entity from a facet,
the entity_attach MCP tool now returns an error instead of silently
re-activating it. This respects user decisions - if they trashed an
entity, agents shouldn't override that choice.

The web UI route remains unchanged, allowing users to explicitly
re-attach via the star button if they change their mind.

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

+47 -24
+12 -24
apps/entities/tools.py
··· 164 164 facets/{facet}/entities.jsonl. Attached entities are long-term tracked 165 165 entities that appear in facet summaries and agent context. 166 166 167 - If a previously detached entity with the same type+name exists, 168 - re-activates it instead of creating a duplicate. 167 + If the entity was previously detached (removed by the user), this tool 168 + will return an error - the user intentionally removed it, so agents 169 + should not re-attach it automatically. Users can re-attach manually 170 + via the web UI if they change their mind. 169 171 170 172 Sets attached_at and updated_at timestamps on the new entity. 171 173 ··· 180 182 - facet: The facet name 181 183 - message: Success message 182 184 - entity: The attached entity details (type, name, description) 183 - - reattached: True if a detached entity was re-activated 184 185 185 186 Examples: 186 187 - entity_attach("personal", "Person", "Alice", "Close friend from college") ··· 201 202 for entity in existing: 202 203 if entity.get("type") == type and entity.get("name") == name: 203 204 if entity.get("detached"): 204 - # Re-activate detached entity 205 - entity.pop("detached", None) 206 - entity["updated_at"] = int(time.time() * 1000) 207 - entity["description"] = description 208 - save_entities(facet, existing, day=None) 209 - 210 - log_tool_action( 211 - facet=facet, 212 - action="entity_reattach", 213 - params={"type": type, "name": name, "description": description}, 214 - context=context, 215 - ) 216 - 205 + # User intentionally removed this entity - don't re-attach 217 206 return { 218 - "facet": facet, 219 - "message": f"Entity '{name}' re-attached successfully", 220 - "entity": { 221 - "type": type, 222 - "name": name, 223 - "description": description, 224 - }, 225 - "reattached": True, 207 + "error": f"Entity '{name}' was previously removed by the user", 208 + "suggestion": ( 209 + "The user intentionally detached this entity from the " 210 + f"'{facet}' facet. Either it's the wrong facet for this " 211 + "entity, or it's not important to them. Do not attempt " 212 + "to re-attach it." 213 + ), 226 214 } 227 215 else: 228 216 return {
+35
tests/test_mcp_tools.py
··· 306 306 assert now - postgres["attached_at"] < 60000 307 307 # Initially both should be equal 308 308 assert postgres["attached_at"] == postgres["updated_at"] 309 + 310 + 311 + def test_entity_attach_blocks_detached_entity(): 312 + """Test that entity_attach returns error for detached (user-removed) entities.""" 313 + # Entity was previously attached but user removed it (detached=True) 314 + mock_entities = [ 315 + { 316 + "type": "Person", 317 + "name": "Bob Smith", 318 + "description": "Former contact", 319 + "detached": True, 320 + "attached_at": 1700000000000, 321 + "updated_at": 1700000001000, 322 + }, 323 + ] 324 + 325 + with ( 326 + patch("apps.entities.tools.load_entities") as mock_load, 327 + patch("apps.entities.tools.save_entities") as mock_save, 328 + patch("apps.entities.tools.is_valid_entity_type") as mock_validate, 329 + ): 330 + mock_validate.return_value = True 331 + mock_load.return_value = mock_entities 332 + result = entity_tools.entity_attach( 333 + "work", "Person", "Bob Smith", "Trying to re-add" 334 + ) 335 + 336 + # Should return error, not re-attach 337 + assert "error" in result 338 + assert "previously removed" in result["error"] 339 + assert "suggestion" in result 340 + assert "intentionally" in result["suggestion"].lower() 341 + 342 + # Should NOT have saved anything 343 + mock_save.assert_not_called()