personal memory agent
0
fork

Configure Feed

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

Commit concurrent session changes (entities, observer, talent updates)

+178 -83
+19 -70
apps/entities/call.py
··· 13 13 import typer 14 14 15 15 from think.entities.core import entity_slug, is_valid_entity_type 16 - from think.entities.journal import ( 17 - get_or_create_journal_entity, 18 - load_journal_entity, 19 - save_journal_entity, 20 - ) 21 - from think.entities.loading import load_entities 16 + from think.entities.loading import clear_entity_loading_cache, load_entities 22 17 from think.entities.matching import resolve_entity, validate_aka_uniqueness 23 18 from think.entities.observations import ( 24 19 add_observation, ··· 26 21 save_observations, 27 22 ) 28 23 from think.entities.relationships import ( 24 + clear_relationship_caches, 29 25 entity_memory_path, 30 26 load_facet_relationship, 31 27 save_facet_relationship, 32 28 ) 29 + from think.entities.journal import ( 30 + clear_journal_entity_cache, 31 + get_or_create_journal_entity, 32 + load_journal_entity, 33 + save_journal_entity, 34 + ) 33 35 from think.entities.saving import ( 34 36 save_detected_entity, 35 37 update_detected_entity, ··· 44 46 45 47 app = typer.Typer(help="Entity management.") 46 48 47 - # Simple in-memory cache for entity and observation loading 48 - ENTITY_CACHE = {} 49 - OBSERVATION_CACHE = {} 49 + 50 + def _clear_all_caches(): 51 + """Clear all underlying think entity caches.""" 52 + clear_entity_loading_cache() 53 + clear_relationship_caches() 54 + clear_journal_entity_cache() 50 55 51 - def clear_entity_caches(): 52 - """Clears the entity and observation caches.""" 53 - ENTITY_CACHE.clear() 54 - OBSERVATION_CACHE.clear() 55 56 56 57 def _resolve_or_exit(facet: str, entity: str) -> dict: 57 58 """Resolve entity or exit with CLI error.""" ··· 96 97 ) -> None: 97 98 """List entities for a facet.""" 98 99 facet = resolve_sol_facet(facet) 99 - 100 - # Use cache for attached entities (day is None) 101 - if day is None: 102 - cache_key = (facet, day, False, False) # Default values for include_detached, include_blocked 103 - if cache_key in ENTITY_CACHE: 104 - # print(f"Cache HIT: entities {cache_key}") # Debugging 105 - entities = ENTITY_CACHE[cache_key] 106 - else: 107 - # print(f"Cache MISS: entities {cache_key}") # Debugging 108 - entities = load_entities(facet, day) # Original call 109 - ENTITY_CACHE[cache_key] = entities 110 - else: 111 - # No caching for detected entities (day is provided) 112 - entities = load_entities(facet, day) 100 + entities = load_entities(facet, day) 113 101 114 102 if not entities: 115 103 typer.echo("No entities found.") ··· 164 152 if src_relationship is not None and dst_relationship is None: 165 153 save_facet_relationship(to_facet, entity_id, src_relationship) 166 154 167 - # Use cache for observations 168 - cache_key_src = (from_facet, entity_name) 169 - if cache_key_src in OBSERVATION_CACHE: 170 - src_obs = OBSERVATION_CACHE[cache_key_src] 171 - else: 172 - src_obs = load_observations(from_facet, entity_name) 173 - OBSERVATION_CACHE[cache_key_src] = src_obs 174 - 175 - cache_key_dst = (to_facet, entity_name) 176 - if cache_key_dst in OBSERVATION_CACHE: 177 - dst_obs = OBSERVATION_CACHE[cache_key_dst] 178 - else: 179 - dst_obs = load_observations(to_facet, entity_name) 180 - OBSERVATION_CACHE[cache_key_dst] = dst_obs 155 + src_obs = load_observations(from_facet, entity_name) 156 + dst_obs = load_observations(to_facet, entity_name) 181 157 182 158 existing_keys = {(o["content"], o.get("observed_at")) for o in dst_obs} 183 159 merged = list(dst_obs) + [ ··· 186 162 if (o["content"], o.get("observed_at")) not in existing_keys 187 163 ] 188 164 save_observations(to_facet, entity_name, merged) 189 - # Invalidate cache for dst_obs as it has been updated 190 - if cache_key_dst in OBSERVATION_CACHE: 191 - del OBSERVATION_CACHE[cache_key_dst] 192 165 193 166 shutil.rmtree(str(src_dir)) 194 167 else: ··· 413 386 return 414 387 415 388 # Validate uniqueness across all entities in facet 416 - # Use cache for load_entities call 417 - cache_key = (facet, None, True, True) # Equivalent to day=None, include_detached=True, include_blocked=True 418 - if cache_key in ENTITY_CACHE: 419 - # print(f"Cache HIT: entities for aka uniqueness check {cache_key}") # Debugging 420 - entities = ENTITY_CACHE[cache_key] 421 - else: 422 - # print(f"Cache MISS: entities for aka uniqueness check {cache_key}") # Debugging 423 - entities = load_entities(facet, day=None, include_detached=True, include_blocked=True) 424 - ENTITY_CACHE[cache_key] = entities 389 + entities = load_entities(facet, day=None, include_detached=True, include_blocked=True) 425 390 426 391 conflict = validate_aka_uniqueness( 427 392 aka_value, entities, exclude_entity_name=resolved_name ··· 461 426 facet = resolve_sol_facet(facet) 462 427 resolved = _resolve_or_exit(facet, entity) 463 428 resolved_name = resolved.get("name", "") 464 - 465 - # Use cache for observations 466 - cache_key = (facet, resolved_name) 467 - if cache_key in OBSERVATION_CACHE: 468 - # print(f"Cache HIT: observations {cache_key}") # Debugging 469 - obs = OBSERVATION_CACHE[cache_key] 470 - else: 471 - # print(f"Cache MISS: observations {cache_key}") # Debugging 472 - obs = load_observations(facet, resolved_name) 473 - OBSERVATION_CACHE[cache_key] = obs 429 + obs = load_observations(facet, resolved_name) 474 430 475 431 if not obs: 476 432 typer.echo(f"No observations for '{resolved_name}'.") ··· 497 453 498 454 try: 499 455 add_observation(facet, resolved_name, content, source_day) 500 - 501 - # Invalidate cache for this observation to ensure fresh data on next read 502 - cache_key = (facet, resolved_name) 503 - if cache_key in OBSERVATION_CACHE: 504 - del OBSERVATION_CACHE[cache_key] 505 - # print(f"Cache INVALIDATED: observations {cache_key} after add_observation") # Debugging 506 - 507 456 except ValueError as exc: 508 457 typer.echo(f"Error: {exc}", err=True) 509 458 raise typer.Exit(1)
+5 -1
apps/todos/talent/daily.md
··· 8 8 "schedule": "daily", 9 9 "priority": 50, 10 10 "multi_facet": true, 11 - "group": "Todos" 11 + "group": "Todos", 12 + "load": { 13 + "agents": True, 14 + "journal": True 15 + } 12 16 } 13 17 14 18 $sol_identity
+3 -1
convey/root.py
··· 98 98 "app:observer.ingest_transfer", 99 99 "app:observer.ingest_manifest", 100 100 "app:observer.ingest_manifest_day", 101 - # Journal-source manifest endpoints use key-based auth, not session 101 + # Journal-source manifest and ingest endpoints use key-based auth, not session 102 102 "app:import.journal_source_manifest", 103 + "app:import.ingest_segments", 104 + "app:import.ingest_entities", 103 105 }: 104 106 return None 105 107
+8 -2
observe/describe.py
··· 307 307 f"{len(self.qualified_frames)} qualified" 308 308 ) 309 309 310 + except av.error.InvalidDataError as e: 311 + logger.error( 312 + f"Invalid video data error for {self.video_path}: {e}. Skipping video.", 313 + exc_info=True 314 + ) 315 + return [] 310 316 except Exception as e: 311 317 logger.error( 312 - f"Error processing video {self.video_path}: {e}", exc_info=True 318 + f"Unexpected error processing video {self.video_path}: {e}", 319 + exc_info=True 313 320 ) 314 321 raise 315 - 316 322 return self.qualified_frames 317 323 318 324 def _dhash(self, img: Image.Image) -> int:
+7 -1
observe/observer_client.py
··· 177 177 if not self._key: 178 178 return UploadResult(False) 179 179 180 - url = f"{self._url}/app/observer/ingest/{self._key}" 180 + url = f"{self._url}/app/observer/ingest" 181 181 for attempt, delay in enumerate(RETRY_BACKOFF): 182 182 file_handles = [] 183 183 files_data = [] ··· 206 206 data["platform"] = self._platform 207 207 if meta: 208 208 data["meta"] = json.dumps(meta) 209 + 210 + headers = {} 211 + if self._key: 212 + headers["Authorization"] = f"Bearer {self._key}" 213 + logger.debug(f"Sending Authorization header: Bearer {self._key[:8]}...") 209 214 210 215 response = self._session.post( 211 216 url, 212 217 data=data, 213 218 files=files_data, 219 + headers=headers, 214 220 timeout=UPLOAD_TIMEOUT, 215 221 ) 216 222
+1 -1
sol/agency.md
··· 13 13 ## curation 14 14 - [ ] 2026-04-14 unknown recurring speaker: cluster 128 (164 samples, plaud), cluster 84 (105 samples, pro5e), cluster 33 (91 samples, plaud). 15 15 - [ ] 2026-04-14 unknown recurring speaker: cluster 279 (67 samples, fedora), cluster 118 (51 samples, fedora). 16 - - [ ] 2026-04-14 entity duplicates: `Jeremie Miller` vs `Jer`. 16 + - [x] 2026-04-14 entity duplicates: `Jeremie Miller` vs `Jer`. Resolved 2026-04-14. 17 17 - [ ] 2026-04-08 speaker curation: cluster 11 (36 samples) from pro5e. cluster 18 (91 samples) remains active across Plaud imports. 18 18 - [ ] 2026-04-08 entity duplicates: `Zoey` in personal facet; `Solstone` vs `Sunstone` in solstone facet.
+5 -1
talent/facet_newsletter.md
··· 7 7 "color": "#0d47a1", 8 8 "schedule": "daily", 9 9 "priority": 40, 10 - "multi_facet": true 10 + "multi_facet": true, 11 + "load": { 12 + "agents": True, 13 + "journal": True 14 + } 11 15 } 12 16 13 17 $sol_identity
+33
think/entities/journal.py
··· 18 18 from think.entities.core import EntityDict, atomic_write, get_identity_names 19 19 from think.utils import get_journal, now_ms 20 20 21 + # Global cache for journal entities: {entity_id: EntityDict} 22 + _JOURNAL_ENTITY_CACHE: dict[str, EntityDict] | None = None 23 + 24 + 25 + def clear_journal_entity_cache() -> None: 26 + """Clear the journal entity cache.""" 27 + global _JOURNAL_ENTITY_CACHE 28 + _JOURNAL_ENTITY_CACHE = None 29 + 21 30 22 31 def journal_entity_path(entity_id: str) -> Path: 23 32 """Return path to journal-level entity file. ··· 41 50 Entity dict with id, name, type, aka, is_principal, created_at fields, 42 51 or None if not found. 43 52 """ 53 + global _JOURNAL_ENTITY_CACHE 54 + if _JOURNAL_ENTITY_CACHE is not None and entity_id in _JOURNAL_ENTITY_CACHE: 55 + return _JOURNAL_ENTITY_CACHE[entity_id] 56 + 44 57 path = journal_entity_path(entity_id) 45 58 if not path.exists(): 46 59 return None ··· 50 63 data = json.load(f) 51 64 # Ensure id is present 52 65 data["id"] = entity_id 66 + 67 + # Update cache if it exists (single entity load doesn't populate full cache) 68 + if _JOURNAL_ENTITY_CACHE is not None: 69 + _JOURNAL_ENTITY_CACHE[entity_id] = data 70 + 53 71 return data 54 72 except (json.JSONDecodeError, OSError): 55 73 return None ··· 70 88 entity_id = entity.get("id") 71 89 if not entity_id: 72 90 raise ValueError("Entity must have an 'id' field") 91 + 92 + # Clear cache on modification 93 + clear_journal_entity_cache() 73 94 74 95 path = journal_entity_path(entity_id) 75 96 content = json.dumps(entity, ensure_ascii=False, indent=2) + "\n" ··· 102 123 Returns: 103 124 Dict mapping entity_id to entity dict 104 125 """ 126 + global _JOURNAL_ENTITY_CACHE 127 + if _JOURNAL_ENTITY_CACHE is not None: 128 + return _JOURNAL_ENTITY_CACHE 129 + 105 130 entity_ids = scan_journal_entities() 106 131 entities = {} 107 132 for entity_id in entity_ids: 108 133 entity = load_journal_entity(entity_id) 109 134 if entity: 110 135 entities[entity_id] = entity 136 + 137 + _JOURNAL_ENTITY_CACHE = entities 111 138 return entities 112 139 113 140 ··· 243 270 if journal_entity.get("is_principal"): 244 271 raise ValueError("Cannot block the principal (self) entity") 245 272 273 + # Clear cache on modification 274 + clear_journal_entity_cache() 275 + 246 276 # Set blocked flag on journal entity 247 277 journal_entity["blocked"] = True 248 278 journal_entity["updated_at"] = now_ms() ··· 323 353 324 354 if journal_entity.get("is_principal"): 325 355 raise ValueError("Cannot delete the principal (self) entity") 356 + 357 + # Clear cache on modification 358 + clear_journal_entity_cache() 326 359 327 360 facets_deleted = [] 328 361
+32 -5
think/entities/loading.py
··· 28 28 ) 29 29 from think.utils import get_journal 30 30 31 + # Global cache for loaded entities: {(facet, day, detached, blocked): list[EntityDict]} 32 + _ENTITY_LOADING_CACHE: dict[tuple, list[EntityDict]] | None = None 33 + 34 + 35 + def clear_entity_loading_cache() -> None: 36 + """Clear the entity loading cache.""" 37 + global _ENTITY_LOADING_CACHE 38 + _ENTITY_LOADING_CACHE = None 39 + 31 40 32 41 def detected_entities_path(facet: str, day: str) -> Path: 33 42 """Return path to detected entities file for a facet and day. ··· 179 188 >>> load_entities("personal") 180 189 [{"id": "john_smith", "type": "Person", "name": "John Smith", "description": "Friend"}] 181 190 """ 191 + global _ENTITY_LOADING_CACHE 192 + 193 + # Use cache if available 194 + cache_key = (facet, day, include_detached, include_blocked) 195 + if _ENTITY_LOADING_CACHE is not None: 196 + cached = _ENTITY_LOADING_CACHE.get(cache_key) 197 + if cached is not None: 198 + return cached 199 + 182 200 # For detected entities, use day-specific files 183 201 if day is not None: 184 202 path = detected_entities_path(facet, day) 185 - return parse_entity_file(str(path)) 203 + result = parse_entity_file(str(path)) 204 + else: 205 + # For attached entities, load from relationships 206 + result = _load_entities_from_relationships( 207 + facet, include_detached=include_detached, include_blocked=include_blocked 208 + ) 186 209 187 - # For attached entities, load from relationships 188 - return _load_entities_from_relationships( 189 - facet, include_detached=include_detached, include_blocked=include_blocked 190 - ) 210 + # Populate cache if initialized 211 + if _ENTITY_LOADING_CACHE is not None: 212 + _ENTITY_LOADING_CACHE[cache_key] = result 213 + else: 214 + # Initialize and populate 215 + _ENTITY_LOADING_CACHE = {cache_key: result} 216 + 217 + return result 191 218 192 219 193 220 def load_all_attached_entities(
+25
think/entities/observations.py
··· 21 21 from think.entities.relationships import entity_memory_path 22 22 from think.utils import now_ms 23 23 24 + # Global cache for entity observations: {(facet, entity_slug): list[dict]} 25 + _OBSERVATION_CACHE: dict[tuple[str, str], list[dict[str, Any]]] | None = None 26 + 27 + 28 + def clear_observation_cache() -> None: 29 + """Clear the entity observation cache.""" 30 + global _OBSERVATION_CACHE 31 + _OBSERVATION_CACHE = None 32 + 24 33 25 34 def observations_file_path(facet: str, name: str) -> Path: 26 35 """Return path to observations file for an entity. ··· 57 66 >>> load_observations("work", "Alice Johnson") 58 67 [{"content": "Prefers async communication", "observed_at": 1736784000000, "source_day": "20250113"}] 59 68 """ 69 + global _OBSERVATION_CACHE 70 + from think.entities.core import entity_slug 71 + 72 + slug = entity_slug(name) 73 + if _OBSERVATION_CACHE is not None: 74 + cached = _OBSERVATION_CACHE.get((facet, slug)) 75 + if cached is not None: 76 + return cached 77 + 60 78 path = observations_file_path(facet, name) 61 79 62 80 if not path.exists(): ··· 74 92 except json.JSONDecodeError: 75 93 continue # Skip malformed lines 76 94 95 + # Update cache if initialized 96 + if _OBSERVATION_CACHE is not None: 97 + _OBSERVATION_CACHE[(facet, slug)] = observations 98 + 77 99 return observations 78 100 79 101 ··· 87 109 name: Entity name 88 110 observations: List of observation dictionaries 89 111 """ 112 + # Clear cache on modification 113 + clear_observation_cache() 114 + 90 115 path = observations_file_path(facet, name) 91 116 92 117 # Format observations as JSONL
+40 -1
think/entities/relationships.py
··· 20 20 from think.entities.core import EntityDict, atomic_write, entity_slug 21 21 from think.utils import get_journal 22 22 23 + # Global cache for facet relationships: {(facet, entity_id): EntityDict} 24 + _RELATIONSHIP_CACHE: dict[tuple[str, str], EntityDict] | None = None 25 + # Global cache for facet relationship IDs: {facet: [entity_id, ...]} 26 + _RELATIONSHIP_IDS_CACHE: dict[str, list[str]] | None = None 27 + 28 + 29 + def clear_relationship_caches() -> None: 30 + """Clear all relationship and ID caches.""" 31 + global _RELATIONSHIP_CACHE, _RELATIONSHIP_IDS_CACHE 32 + _RELATIONSHIP_CACHE = None 33 + _RELATIONSHIP_IDS_CACHE = None 34 + 23 35 24 36 def facet_relationship_path(facet: str, entity_id: str) -> Path: 25 37 """Return path to facet relationship file. ··· 47 59 Relationship dict with entity_id, description, timestamps, etc., 48 60 or None if not found. 49 61 """ 62 + global _RELATIONSHIP_CACHE 63 + if _RELATIONSHIP_CACHE is not None: 64 + cached = _RELATIONSHIP_CACHE.get((facet, entity_id)) 65 + if cached is not None: 66 + return cached 67 + 50 68 path = facet_relationship_path(facet, entity_id) 51 69 if not path.exists(): 52 70 return None ··· 56 74 data = json.load(f) 57 75 # Ensure entity_id is present 58 76 data["entity_id"] = entity_id 77 + 78 + # Update cache if initialized 79 + if _RELATIONSHIP_CACHE is not None: 80 + _RELATIONSHIP_CACHE[(facet, entity_id)] = data 81 + 59 82 return data 60 83 except (json.JSONDecodeError, OSError): 61 84 return None ··· 73 96 entity_id: Entity ID (slug) 74 97 relationship: Relationship dict with description, timestamps, etc. 75 98 """ 99 + # Clear caches on modification 100 + clear_relationship_caches() 101 + 76 102 path = facet_relationship_path(facet, entity_id) 77 103 78 104 # Ensure entity_id is in the relationship ··· 93 119 Returns: 94 120 List of entity IDs (directory names) 95 121 """ 122 + global _RELATIONSHIP_IDS_CACHE 123 + if _RELATIONSHIP_IDS_CACHE is not None: 124 + cached = _RELATIONSHIP_IDS_CACHE.get(facet) 125 + if cached is not None: 126 + return cached 127 + 96 128 entities_dir = Path(get_journal()) / "facets" / facet / "entities" 97 129 if not entities_dir.exists(): 98 130 return [] ··· 102 134 if entry.is_dir() and (entry / "entity.json").exists(): 103 135 entity_ids.append(entry.name) 104 136 105 - return sorted(entity_ids) 137 + entity_ids.sort() 138 + if _RELATIONSHIP_IDS_CACHE is None: 139 + _RELATIONSHIP_IDS_CACHE = {} 140 + _RELATIONSHIP_IDS_CACHE[facet] = entity_ids 141 + return entity_ids 106 142 107 143 108 144 def enrich_relationship_with_journal( ··· 217 253 218 254 if new_folder.exists(): 219 255 raise OSError(f"Target folder already exists: {new_folder}") 256 + 257 + # Clear caches on modification 258 + clear_relationship_caches() 220 259 221 260 shutil.move(str(old_folder), str(new_folder)) 222 261 return True