personal memory agent
0
fork

Configure Feed

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

Add dynamic input-level context for quiet/sparse journal days

Insights and scheduled agents now receive context about data availability
for the day being processed. This helps them handle days with no recordings
(holidays, weekends, offline) gracefully without raising false alarms.

Changes:
- Add day_input_summary() helper to think/utils.py that detects recording
segments and returns human-readable summaries like "No recordings" or
"Light activity: 2 segments, ~3 minutes"
- Modify think/insight.py to prepend input context notes to Gemini prompts
when file_count is 0 or very low
- Modify think/supervisor.py to include input summary in agent spawn prompts
- Update test_supervisor_schedule.py to mock the new function

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

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

+84 -8
+13 -5
tests/test_supervisor_schedule.py
··· 9 9 from think.supervisor import spawn_scheduled_agents 10 10 11 11 12 + @patch("think.supervisor.day_input_summary") 12 13 @patch("think.supervisor.cortex_request") 13 14 @patch("think.supervisor.get_agents") 14 15 @pytest.mark.asyncio 15 - async def test_spawn_scheduled_agents(mock_get_agents, mock_cortex_request): 16 + async def test_spawn_scheduled_agents( 17 + mock_get_agents, mock_cortex_request, mock_input_summary 18 + ): 16 19 """Test that scheduled agents are spawned correctly via Cortex.""" 17 20 from think.supervisor import check_scheduled_agents 18 21 ··· 37 40 # Mock cortex_request to return agent IDs 38 41 mock_cortex_request.side_effect = ["123456789", "987654321"] 39 42 43 + # Mock input summary 44 + mock_input_summary.return_value = "No recordings" 45 + 40 46 # Call the functions (prepare then execute) 41 47 with patch.dict(os.environ, {"JOURNAL_PATH": "/test/journal"}, clear=True): 42 48 spawn_scheduled_agents() ··· 45 51 # Should spawn 2 agents (todo and another_daily) 46 52 assert mock_cortex_request.call_count == 2 47 53 48 - # Check first request call (todos:todo) - now simplified 54 + # Check first request call (todos:todo) - includes input summary 49 55 first_call = mock_cortex_request.call_args_list[0] 50 56 assert first_call[1]["persona"] == "todos:todo" 51 - assert "Running daily scheduled task for todos:todo" in first_call[1]["prompt"] 57 + assert "Running daily scheduled task" in first_call[1]["prompt"] 58 + assert "No recordings" in first_call[1]["prompt"] 52 59 53 - # Check second request call (another_daily) - now simplified 60 + # Check second request call (another_daily) - includes input summary 54 61 second_call = mock_cortex_request.call_args_list[1] 55 62 assert second_call[1]["persona"] == "another_daily" 56 - assert "Running daily scheduled task for another_daily" in second_call[1]["prompt"] 63 + assert "Running daily scheduled task" in second_call[1]["prompt"] 64 + assert "No recordings" in second_call[1]["prompt"] 57 65 58 66 59 67 @patch("think.supervisor.check_scheduled_agents")
+15
think/insight.py
··· 355 355 markdown, file_count = cluster(args.day) 356 356 day_dir = str(day_path(args.day)) 357 357 358 + # Prepend input context note for sparse/empty days 359 + if file_count == 0: 360 + input_note = ( 361 + "**Input Note:** No recordings for this day - likely a holiday, " 362 + "weekend, or offline day. This is normal. Generate concise " 363 + "acknowledgment rather than detailed analysis.\n\n" 364 + ) 365 + markdown = input_note + markdown 366 + elif file_count < 3: 367 + input_note = ( 368 + "**Input Note:** Limited recordings for this day. " 369 + "Scale analysis to available input.\n\n" 370 + ) 371 + markdown = input_note + markdown 372 + 358 373 try: 359 374 360 375 load_dotenv()
+4 -3
think/supervisor.py
··· 18 18 from think.callosum import CallosumConnection, CallosumServer 19 19 from think.facets import get_active_facets, get_facets 20 20 from think.runner import ManagedProcess as RunnerManagedProcess 21 - from think.utils import get_agents, setup_cli 21 + from think.utils import day_input_summary, get_agents, setup_cli 22 22 23 23 DEFAULT_THRESHOLD = 60 24 24 CHECK_INTERVAL = 30 ··· 422 422 # yesterday is the same for all agents in the group (YYYY-MM-DD format) 423 423 yesterday = agents_list[0][2] if agents_list else "" 424 424 yesterday_yyyymmdd = yesterday.replace("-", "") 425 + input_summary = day_input_summary(yesterday_yyyymmdd) 425 426 facets = get_facets() 426 427 enabled_facets = {k: v for k, v in facets.items() if not v.get("muted", False)} 427 428 active_facets = get_active_facets(yesterday_yyyymmdd) ··· 454 455 455 456 logging.info(f"Spawning {persona_id} for facet: {facet_name}") 456 457 agent_id = cortex_request( 457 - prompt=f"You are processing facet '{facet_name}' for yesterday ({yesterday}), use get_facet('{facet_name}') to load the correct context before starting.", 458 + prompt=f"Processing facet '{facet_name}' for yesterday ({yesterday}): {input_summary}. Use get_facet('{facet_name}') to load context.", 458 459 persona=persona_id, 459 460 ) 460 461 active_files.append(agents_dir / f"{agent_id}_active.jsonl") ··· 464 465 else: 465 466 # Regular single-instance agent 466 467 agent_id = cortex_request( 467 - prompt=f"Running daily scheduled task for {persona_id}, yesterday was {yesterday}.", 468 + prompt=f"Running daily scheduled task. Yesterday ({yesterday}): {input_summary}.", 468 469 persona=persona_id, 469 470 ) 470 471 active_files.append(agents_dir / f"{agent_id}_active.jsonl")
+52
think/utils.py
··· 410 410 _append_task_log(journal, message) 411 411 412 412 413 + def day_input_summary(day: str) -> str: 414 + """Return a human-readable summary of recording data available for a day. 415 + 416 + Uses cluster_segments() to detect recording segments and computes 417 + total duration from segment keys (HHMMSS_LEN format). 418 + 419 + Parameters 420 + ---------- 421 + day: 422 + Day in YYYYMMDD format. 423 + 424 + Returns 425 + ------- 426 + str 427 + Human-readable summary like "No recordings", "Light activity: 2 segments, 428 + ~3 minutes", or "18 segments, ~7.5 hours". 429 + """ 430 + from think.cluster import cluster_segments 431 + 432 + segments = cluster_segments(day) 433 + 434 + if not segments: 435 + return "No recordings" 436 + 437 + # Compute total duration from segment keys (HHMMSS_LEN format) 438 + total_seconds = 0 439 + for seg in segments: 440 + key = seg.get("key", "") 441 + if "_" in key: 442 + parts = key.split("_") 443 + if len(parts) >= 2 and parts[1].isdigit(): 444 + total_seconds += int(parts[1]) 445 + 446 + # Format duration 447 + if total_seconds < 60: 448 + duration_str = f"~{total_seconds} seconds" 449 + elif total_seconds < 3600: 450 + minutes = total_seconds / 60 451 + duration_str = f"~{minutes:.0f} minutes" 452 + else: 453 + hours = total_seconds / 3600 454 + duration_str = f"~{hours:.1f} hours" 455 + 456 + segment_count = len(segments) 457 + 458 + # Categorize activity level 459 + if segment_count < 5 or total_seconds < 1800: # < 5 segments or < 30 min 460 + return f"Light activity: {segment_count} segment{'s' if segment_count != 1 else ''}, {duration_str}" 461 + else: 462 + return f"{segment_count} segments, {duration_str}" 463 + 464 + 413 465 def touch_health(name: str) -> None: 414 466 """Update the journal's ``name`` heartbeat file. 415 467