personal memory agent
0
fork

Configure Feed

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

Fix generator race condition and audit cleanup

- Fix get_agent_end_state to check file contents for terminal events
even when file is still _active.jsonl (Callosum broadcasts before rename)
- Fix SEGMENT_KEY extraction path in cortex token logging
- Remove redundant typing import in cortex.py
- Update docs: WebSocket → Callosum, think.mcp_tools → think.mcp
- Rename all "insights" terminology to "generators" across codebase

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

+46 -43
+6 -6
docs/CORTEX.md
··· 40 40 { 41 41 "event": "request", 42 42 "ts": 1234567890123, // Required: millisecond timestamp (must match filename) 43 - "prompt": "Analyze this code for security issues", // Required: the task or question 43 + "prompt": "Analyze this code for security issues", // Required for agents (not generators) 44 44 "name": "default", // Optional: agent name from muse/*.md 45 45 "provider": "openai", // Optional: override provider (openai, google, anthropic) 46 46 "max_output_tokens": 8192, // Optional: maximum response tokens ··· 48 48 "disable_mcp": false, // Optional: disable MCP tools for this request 49 49 "continue_from": "1234567890122", // Optional: continue from previous agent 50 50 "facet": "my-project", // Optional: project context 51 - "output": "md", // Optional: output format ("md" or "json"), writes to insights/ 51 + "output": "md", // Optional: output format ("md" or "json"), writes to agents/ 52 52 "day": "20250109", // Optional: YYYYMMDD format, defaults to current day 53 53 "env": { // Optional: environment variables for subprocess 54 54 "API_KEY": "secret", ··· 241 241 242 242 ## Agent Output 243 243 244 - When an agent completes successfully, its result can be automatically written to a file. This uses the same output path logic as insights. 244 + When an agent completes successfully, its result can be automatically written to a file. This uses the same output path logic as generators. 245 245 246 246 - Include an `output` field in the agent's frontmatter with the format ("md" or "json") 247 247 - Output path is derived from agent name + format + schedule: ··· 270 270 1. Cortex loads the agent configuration using `get_agent()` from `think/utils.py` 271 271 2. The configuration is built with three instruction components: 272 272 - `system_instruction`: `journal.md` (shared base prompt, cacheable) 273 - - `extra_context`: Runtime context (facets, insights list, datetime) 273 + - `extra_context`: Runtime context (facets, generators list, datetime) 274 274 - `user_instruction`: The agent's `.md` file content 275 275 3. Request parameters override agent defaults in the merged configuration 276 276 4. The full configuration is passed to the agent process ··· 330 330 - **OpenAI, Anthropic, Google**: Full MCP tool support via HTTP transport 331 331 332 332 ### Tool Discovery 333 - MCP tools are provided by the `think.mcp_tools` FastMCP server, which: 333 + MCP tools are provided by the `think.mcp` FastMCP server, which: 334 334 - Runs inside Cortex as a background HTTP service 335 335 - Shares its URL directly with agent runs (`mcp_server_url`) so no discovery file is needed 336 336 - Exposes journal search and retrieval capabilities ··· 398 398 - Starts and monitors the MCP tools HTTP server 399 399 - Handles process restarts on failure 400 400 - Monitors system health indicators 401 - - Triggers `sol dream` at midnight for daily processing (insights + agents) 401 + - Triggers `sol dream` at midnight for daily processing (generators + agents) 402 402 403 403 This is distinct from agent lifecycle management, which Cortex handles internally through file state transitions.
+2 -2
docs/THINK.md
··· 20 20 - `sol dream` runs generators and agents for a single day via Cortex. 21 21 - `sol supervisor` monitors observation heartbeats. Use `--no-observers` to disable local capture (sense still runs for remote uploads and imports). 22 22 - `sol mcp` starts an MCP server exposing search capabilities for both summary text and raw transcripts. 23 - - `sol cortex` starts a WebSocket API server for managing AI agent instances and generators. 23 + - `sol cortex` starts a Callosum-based service for managing AI agent instances and generators. 24 24 25 25 ```bash 26 26 sol cluster YYYYMMDD [--start HHMMSS --length MINUTES] ··· 173 173 174 174 ## Cortex API 175 175 176 - Cortex is the central agent management system that all agent spawning should go through. See [CORTEX.md](CORTEX.md) for complete documentation of the Cortex WebSocket API and agent event structures. 176 + Cortex is the central agent management system that all agent spawning should go through. See [CORTEX.md](CORTEX.md) for complete documentation of the Cortex API and agent event structures. 177 177 178 178 ### Using cortex_client 179 179
+5 -5
think/cluster.py
··· 361 361 sources: Optional dict with keys "audio", "screen", "agents" (bools). 362 362 Defaults to {"audio": True, "screen": False, "agents": True}. 363 363 """ 364 - # Default sources for daily insights: audio + insight summaries, no raw screen 364 + # Default sources for daily generators: audio + agent summaries, no raw screen 365 365 if sources is None: 366 366 sources = {"audio": True, "screen": False, "agents": True} 367 367 ··· 391 391 ) -> Tuple[str, int]: 392 392 """Return Markdown summary for one segment's JSON files and the number processed. 393 393 394 - By default uses raw screen data for segment insights (more granular than summaries). 394 + By default uses raw screen data for segment generators (more granular than summaries). 395 395 Override with sources parameter. 396 396 397 397 Args: ··· 403 403 Returns: 404 404 (markdown, file_count) tuple 405 405 """ 406 - # Default sources for segment insights: audio + raw screen, no insight summaries 406 + # Default sources for segment generators: audio + raw screen, no agent summaries 407 407 if sources is None: 408 408 sources = {"audio": True, "screen": True, "agents": False} 409 409 ··· 455 455 ) -> Tuple[str, int]: 456 456 """Return Markdown summary for multiple segments and the number of entries processed. 457 457 458 - By default uses raw screen data for segment insights (more granular than summaries). 458 + By default uses raw screen data for segment generators (more granular than summaries). 459 459 Validates all segments exist before processing; raises ValueError if any are missing. 460 460 461 461 Args: ··· 470 470 Raises: 471 471 ValueError: If any segment directories are missing 472 472 """ 473 - # Default sources for segment insights: audio + raw screen, no insight summaries 473 + # Default sources for segment generators: audio + raw screen, no agent summaries 474 474 if sources is None: 475 475 sources = {"audio": True, "screen": True, "agents": False} 476 476
+12 -12
think/cortex.py
··· 281 281 self.logger.error("Invalid request format: missing 'request' event") 282 282 return 283 283 284 - # Validate prompt early 285 - prompt = request.get("prompt") 286 - if not prompt: 287 - self.logger.error(f"Empty prompt in request {agent_id}") 288 - self._write_error_and_complete(file_path, "Empty prompt in request") 289 - return 290 - 291 284 # Validate and link continue_from if specified 292 285 continue_from = request.get("continue_from") 293 286 if continue_from: ··· 370 363 has_output = bool(config.get("output")) 371 364 372 365 if has_tools: 366 + # Agents require a prompt (generators don't - they use transcripts) 367 + prompt = config.get("prompt") 368 + if not prompt: 369 + self.logger.error(f"Empty prompt in agent request {agent_id}") 370 + self._write_error_and_complete( 371 + file_path, "Empty prompt in agent request" 372 + ) 373 + return 374 + 373 375 # Expand tools if it's a string (tool pack name) 374 376 tools_config = config.get("tools") 375 377 if isinstance(tools_config, str): ··· 620 622 app, name = "system", name 621 623 context = f"agent.{app}.{name}" 622 624 623 - # Extract segment from config env if set 624 - config = original_request.get("config", {}) 625 - env_config = config.get("env", {}) 626 - segment = env_config.get("SEGMENT_KEY") 625 + # Extract segment from env if set (flat merge puts env at top level) 626 + env_config = original_request.get("env", {}) 627 + segment = env_config.get("SEGMENT_KEY") if env_config else None 627 628 628 629 log_token_usage( 629 630 model=model, ··· 956 957 - cost: float | None (calculated cost in USD) 957 958 """ 958 959 from datetime import datetime 959 - from typing import Any 960 960 961 961 from think.models import calc_token_cost 962 962
+9 -6
think/cortex_client.py
··· 224 224 def get_agent_end_state(agent_id: str) -> str: 225 225 """Get how a completed agent ended (finish or error). 226 226 227 + Checks file contents for terminal events even if file is still _active.jsonl, 228 + since Callosum broadcasts happen before file rename. 229 + 227 230 Args: 228 231 agent_id: The agent ID (timestamp) 229 232 230 233 Returns: 231 234 "finish" - Agent completed successfully 232 235 "error" - Agent ended with an error 233 - "running" - Agent is still active 234 - "unknown" - Agent file exists but no terminal event found 236 + "running" - Agent is still active (no terminal event in file) 237 + "unknown" - Agent file not found 235 238 """ 236 239 status = get_agent_log_status(agent_id) 237 - if status == "running": 238 - return "running" 239 240 if status == "not_found": 240 241 return "unknown" 241 242 242 - # Read events to find terminal state 243 + # Read events to find terminal state (even for "running" files that may 244 + # have finish event - Callosum broadcast happens before file rename) 243 245 try: 244 246 events = read_agent_events(agent_id) 245 247 # Find last finish or error event ··· 249 251 return "finish" 250 252 if event_type == "error": 251 253 return "error" 252 - return "unknown" 254 + # No terminal event found - still running 255 + return "running" 253 256 except FileNotFoundError: 254 257 return "unknown" 255 258
+1 -1
think/formatters.py
··· 220 220 files: dict[str, str] = {} 221 221 journal_path = Path(journal) 222 222 223 - # Scan day directories for insights and segment content 223 + # Scan day directories for agent outputs and segment content 224 224 for day, day_abs in day_dirs().items(): 225 225 day_path = Path(day_abs) 226 226
+1 -1
think/models.py
··· 223 223 """ 224 224 contexts = {} 225 225 226 - # System agents from muse/ (agents have "tools" field, insights don't) 226 + # System agents from muse/ (agents have "tools" field, generators don't) 227 227 muse_dir = Path(__file__).parent.parent / "muse" 228 228 if muse_dir.exists(): 229 229 for md_path in muse_dir.glob("*.md"):
+3 -3
think/supervisor.py
··· 958 958 def _run_daily_processing(day: str) -> None: 959 959 """Run complete daily processing via sol dream. 960 960 961 - dream now handles both insights and agent execution, so we just 961 + dream now handles both generators and agent execution, so we just 962 962 invoke it with --force and let it manage the full pipeline. 963 963 964 964 Args: ··· 1036 1036 def _handle_segment_observed(message: dict) -> None: 1037 1037 """Handle segment completion events (from live observation or imports). 1038 1038 1039 - Spawns sol dream in segment mode, which handles both insights and 1039 + Spawns sol dream in segment mode, which handles both generators and 1040 1040 segment agents. 1041 1041 """ 1042 1042 if message.get("tract") != "observe" or message.get("event") != "observed": ··· 1052 1052 1053 1053 logging.info(f"Segment observed: {day}/{segment}, spawning processing...") 1054 1054 1055 - # Run dream in segment mode (handles both insights and agents) 1055 + # Run dream in segment mode (handles both generators and agents) 1056 1056 threading.Thread( 1057 1057 target=_run_segment_processing, 1058 1058 args=(day, segment),
+7 -7
think/utils.py
··· 1072 1072 include_datetime: bool = True, 1073 1073 config_overrides: dict | None = None, 1074 1074 ) -> dict: 1075 - """Compose instruction components for agents or insights. 1075 + """Compose instruction components for agents or generators. 1076 1076 1077 1077 This is the shared function for building system_instruction, user_instruction, 1078 - extra_context, and sources configuration. Both agents and insights use this 1078 + extra_context, and sources configuration. Both agents and generators use this 1079 1079 to ensure consistent prompt composition. 1080 1080 1081 1081 Parameters 1082 1082 ---------- 1083 1083 user_prompt: 1084 1084 Name of the user instruction prompt to load (e.g., "default" for agents). 1085 - If None, no user_instruction is included (typical for insights). 1085 + If None, no user_instruction is included (typical for generators). 1086 1086 user_prompt_dir: 1087 1087 Directory to load user_prompt from. If None, uses think/ directory. 1088 1088 facet: ··· 1090 1090 detailed information for just this facet instead of all facets. 1091 1091 include_datetime: 1092 1092 Whether to include current date/time in extra_context. Default True 1093 - for agents (real-time chat), typically False for insights (past analysis). 1093 + for agents (real-time chat), typically False for generators (past analysis). 1094 1094 config_overrides: 1095 1095 Optional dict from .json "instructions" key. Supported keys: 1096 1096 - "system": prompt name for system instruction (default: "journal") ··· 1362 1362 1363 1363 Scans both system agents (muse/) and app agents (apps/*/muse/). 1364 1364 Agents are identified by having a "tools" field in frontmatter 1365 - (insights have schedule but no tools). 1365 + (generators have schedule but no tools). 1366 1366 System agents use simple keys like "default", while app agents are 1367 1367 namespaced as "app:agent" (e.g., "chat:helper"). 1368 1368 ··· 1380 1380 for md_path in sorted(MUSE_DIR.glob("*.md")): 1381 1381 agent_id = md_path.stem 1382 1382 try: 1383 - # Quick check: load frontmatter to filter out insights 1383 + # Quick check: load frontmatter to filter out generators 1384 1384 post = frontmatter.load(md_path) 1385 1385 if not post.metadata or "tools" not in post.metadata: 1386 1386 continue # This is an insight or hook, not an agent ··· 1404 1404 for md_path in sorted(muse_dir.glob("*.md")): 1405 1405 agent_name = md_path.stem 1406 1406 try: 1407 - # Quick check: load frontmatter to filter out insights 1407 + # Quick check: load frontmatter to filter out generators 1408 1408 post = frontmatter.load(md_path) 1409 1409 if not post.metadata or "tools" not in post.metadata: 1410 1410 continue # This is an insight or hook, not an agent