personal memory agent
0
fork

Configure Feed

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

refactor(entities): split get_or_create_journal_entity into load + create

Delete `get_or_create_journal_entity` and replace with explicit
`create_journal_entity` alongside the existing reader `load_journal_entity`.
Every caller now composes the two at the call site so writes are visible
in the caller's own source:

load_journal_entity(id) or create_journal_entity(id, ...)

This is the L2 foundation of the entity-write-ownership refactor.

Rewritten call sites:
- think/importers/shared.py (seed_entities)
- apps/speakers/bootstrap.py (bootstrap_voiceprints)
- apps/speakers/discovery.py (identify_cluster — reuses existing load)
- apps/speakers/owner.py (confirm_owner_candidate)
- apps/entities/call.py (attach_entity)
- think/entities/saving.py (_save_entities_attached)

+36 -33
+2 -2
apps/entities/call.py
··· 15 15 from think.entities.core import entity_slug, is_valid_entity_type 16 16 from think.entities.journal import ( 17 17 clear_journal_entity_cache, 18 - get_or_create_journal_entity, 18 + create_journal_entity, 19 19 load_journal_entity, 20 20 save_journal_entity, 21 21 ) ··· 282 282 entity_id = entity_slug(name) 283 283 284 284 # Create journal entity (identity record) if it doesn't exist 285 - get_or_create_journal_entity( 285 + load_journal_entity(entity_id) or create_journal_entity( 286 286 entity_id=entity_id, 287 287 name=name, 288 288 entity_type=type_,
+2 -2
apps/speakers/bootstrap.py
··· 34 34 from apps.speakers.owner import load_owner_centroid 35 35 from think.entities import entity_slug, find_matching_entity, is_name_variant_match 36 36 from think.entities.journal import ( 37 + create_journal_entity, 37 38 ensure_journal_entity_memory, 38 - get_or_create_journal_entity, 39 39 load_all_journal_entities, 40 40 load_journal_entity, 41 41 save_journal_entity, ··· 247 247 # Create a new entity for this speaker 248 248 entity_id = entity_slug(speaker_name) 249 249 if not dry_run: 250 - entity = get_or_create_journal_entity( 250 + entity = load_journal_entity(entity_id) or create_journal_entity( 251 251 entity_id=entity_id, 252 252 name=speaker_name, 253 253 entity_type="Person",
+2 -2
apps/speakers/discovery.py
··· 343 343 from apps.speakers.routes import _load_speaker_corrections 344 344 from think.entities import entity_slug, find_matching_entity 345 345 from think.entities.journal import ( 346 - get_or_create_journal_entity, 346 + create_journal_entity, 347 347 load_all_journal_entities, 348 348 load_journal_entity, 349 349 ) ··· 370 370 entity_id = entity_slug(name) 371 371 existing = load_journal_entity(entity_id) 372 372 entity_created = existing is None 373 - entity = get_or_create_journal_entity( 373 + entity = existing or create_journal_entity( 374 374 entity_id=entity_id, 375 375 name=name, 376 376 entity_type="Person",
+5 -3
apps/speakers/owner.py
··· 376 376 from think.entities import entity_slug 377 377 from think.entities.core import get_identity_names 378 378 from think.entities.journal import ( 379 + create_journal_entity, 379 380 ensure_journal_entity_memory, 380 - get_or_create_journal_entity, 381 + load_journal_entity, 381 382 ) 382 383 383 384 candidate_path = _owner_candidate_path() ··· 400 401 if not identity_names: 401 402 return {"error": "No principal entity found"} 402 403 principal_name = identity_names[0] 403 - principal = get_or_create_journal_entity( 404 - entity_id=entity_slug(principal_name), 404 + principal_id = entity_slug(principal_name) 405 + principal = load_journal_entity(principal_id) or create_journal_entity( 406 + entity_id=principal_id, 405 407 name=principal_name, 406 408 entity_type="Person", 407 409 )
+2 -2
think/entities/__init__.py
··· 49 49 # Journal-level entity management 50 50 from think.entities.journal import ( 51 51 block_journal_entity, 52 + create_journal_entity, 52 53 delete_journal_entity, 53 54 ensure_journal_entity_memory, 54 55 get_journal_principal, 55 - get_or_create_journal_entity, 56 56 has_journal_principal, 57 57 journal_entity_memory_path, 58 58 journal_entity_path, ··· 124 124 "is_valid_entity_type", 125 125 # Journal 126 126 "block_journal_entity", 127 + "create_journal_entity", 127 128 "delete_journal_entity", 128 129 "ensure_journal_entity_memory", 129 130 "get_journal_principal", 130 - "get_or_create_journal_entity", 131 131 "has_journal_principal", 132 132 "journal_entity_memory_path", 133 133 "journal_entity_path",
+12 -16
think/entities/journal.py
··· 186 186 return False 187 187 188 188 189 - def get_or_create_journal_entity( 189 + def create_journal_entity( 190 190 entity_id: str, 191 191 name: str, 192 192 entity_type: str, ··· 195 195 *, 196 196 skip_principal: bool = False, 197 197 ) -> EntityDict: 198 - """Get existing journal entity or create new one. 198 + """Create and persist a new journal-level entity. 199 199 200 - If entity exists, returns it unchanged (does not update fields). 201 - If entity doesn't exist, creates it with provided values. 200 + Caller must guarantee the entity does not already exist. Compose with 201 + `load_journal_entity` at the call site: 202 + `load_journal_entity(id) or create_journal_entity(id, ...)`. 202 203 203 204 Args: 204 205 entity_id: Entity ID (slug) 205 - name: Entity name 206 - entity_type: Entity type (e.g., "Person", "Company") 207 - aka: Optional list of aliases 208 - skip_principal: If True, don't flag as principal even if matches identity 206 + name: Display name 207 + entity_type: Entity type (e.g. "Person", "Organization") 208 + aka: Optional list of alternate names 209 + emails: Optional list of email addresses (lowercased on save) 210 + skip_principal: If True, do not auto-flag as principal even when the 211 + name/aka match identity and no principal exists yet. 209 212 210 213 Returns: 211 - The existing or newly created entity dict 214 + The newly-created and persisted entity dict. 212 215 """ 213 - existing = load_journal_entity(entity_id) 214 - if existing: 215 - return existing 216 - 217 - # Create new entity 218 216 entity: EntityDict = { 219 217 "id": entity_id, 220 218 "name": name, ··· 226 224 if emails: 227 225 entity["emails"] = [e.lower() for e in emails] 228 226 229 - # Check if this should be the principal 230 - # Only flag if: matches identity, no existing principal, and not skipped 231 227 if ( 232 228 not skip_principal 233 229 and _should_be_principal(name, aka)
+8 -4
think/entities/saving.py
··· 14 14 import time 15 15 16 16 from think.entities.core import EntityDict, atomic_write, entity_slug 17 - from think.entities.journal import get_or_create_journal_entity, save_journal_entity 17 + from think.entities.journal import ( 18 + create_journal_entity, 19 + load_journal_entity, 20 + save_journal_entity, 21 + ) 18 22 from think.entities.loading import ( 19 23 clear_entity_loading_cache, 20 24 detected_entities_path, ··· 90 94 aka = entity.get("aka") 91 95 is_detached = entity.get("detached", False) 92 96 93 - # Ensure journal entity exists (creates if needed, preserves if exists) 94 - # Skip principal flagging for detached entities 95 - journal_entity = get_or_create_journal_entity( 97 + # Load existing journal entity, or create one. Skip principal 98 + # flagging for detached entities. 99 + journal_entity = load_journal_entity(entity_id) or create_journal_entity( 96 100 entity_id=entity_id, 97 101 name=name, 98 102 entity_type=entity_type,
+3 -2
think/importers/shared.py
··· 667 667 """ 668 668 from think.entities.core import entity_slug 669 669 from think.entities.journal import ( 670 - get_or_create_journal_entity, 670 + create_journal_entity, 671 671 load_all_journal_entities, 672 + load_journal_entity, 672 673 save_journal_entity, 673 674 ) 674 675 from think.entities.matching import find_entity_by_email, find_matching_entity ··· 711 712 # Create new entity 712 713 eid = entity_slug(name) 713 714 emails = [email.lower()] if email else None 714 - new_entity = get_or_create_journal_entity( 715 + new_entity = load_journal_entity(eid) or create_journal_entity( 715 716 entity_id=eid, 716 717 name=name, 717 718 entity_type=entity_type,