personal memory agent
0
fork

Configure Feed

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

Add schedule-aware filtering and summary to sol muse logs

Thread schedule field from muse configs through dream.py request
configs into cortex day-index records. Add --day, --daily, --errors,
and --summary CLI flags to sol muse logs with AND-composition.
--daily uses config fallback for legacy records without schedule field.
--summary shows grouped aggregation with pass/fail counts and runtime.

+228 -12
+13 -3
apps/health/muse/health/SKILL.md
··· 58 58 ## agent runs 59 59 60 60 ```bash 61 - sol muse logs [AGENT] [-c COUNT] 61 + sol muse logs [AGENT] [-c COUNT] [--day YYYYMMDD] [--daily] [--errors] [--summary] 62 62 ``` 63 63 64 64 List recent agent runs. 65 65 66 66 - `AGENT`: optional agent name filter. 67 - - `-c, --count`: max runs shown (default `20`). 67 + - `-c, --count`: max runs shown (default `20`; `50` when `--daily`). 68 + - `--day YYYYMMDD`: show only runs from a specific day. 69 + - `--daily`: show only daily-scheduled runs. 70 + - `--errors`: show only error runs. 71 + - `--summary`: show grouped aggregation instead of individual lines. 72 + 73 + Flags compose with AND logic. For example, `--daily --errors` shows only daily runs that errored. 68 74 69 75 Output columns: agent_id, time, name, status, runtime, cost, events, tools, output_size, model, facet. 70 76 71 - Example: 77 + Examples: 72 78 73 79 ```bash 74 80 sol muse logs 75 81 sol muse logs activity -c 10 82 + sol muse logs --daily 83 + sol muse logs --daily --summary 84 + sol muse logs --day 20260228 85 + sol muse logs --daily --errors 76 86 ``` 77 87 78 88 ## agent run detail
+2 -2
tests/fixtures/journal/agents/20231113.jsonl
··· 1 - {"agent_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite"} 2 - {"agent_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku"} 1 + {"agent_id": "1699900000001", "name": "entities", "day": "20231113", "facet": "personal", "ts": 1699900000001, "status": "completed", "runtime_seconds": 8.4, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "daily"} 2 + {"agent_id": "1699900000002", "name": "flow", "day": "20231113", "facet": null, "ts": 1699900060000, "status": "completed", "runtime_seconds": 4.7, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"}
+3 -3
tests/fixtures/journal/agents/20231114.jsonl
··· 1 - {"agent_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o"} 2 - {"agent_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku"} 3 - {"agent_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite"} 1 + {"agent_id": "1700000000001", "name": "default", "day": "20231114", "facet": null, "ts": 1700000000001, "status": "completed", "runtime_seconds": 0.6, "provider": "openai", "model": "gpt-4o", "schedule": "daily"} 2 + {"agent_id": "1700000000002", "name": "flow", "day": "20231114", "facet": null, "ts": 1700000060000, "status": "error", "runtime_seconds": 13.2, "provider": "anthropic", "model": "claude-3-haiku", "schedule": "segment"} 3 + {"agent_id": "1700000000003", "name": "activity", "day": "20231114", "facet": "work", "ts": 1700000120000, "status": "completed", "runtime_seconds": 6.2, "provider": "google", "model": "gemini-2.5-flash-lite", "schedule": "activity"} 4 4 {"agent_id": "1700000000004", "name": "default", "day": "20231114", "facet": null, "ts": 1700000180000, "status": "completed", "runtime_seconds": 2.1, "provider": "openai", "model": "gpt-4o"}
+99
tests/test_muse_cli.py
··· 377 377 assert dash_count > 0 378 378 379 379 380 + def test_logs_runs_day_filter(capsys): 381 + """--day filters to a specific day.""" 382 + logs_runs(day="20231114") 383 + output = capsys.readouterr().out 384 + lines = [line for line in output.strip().splitlines() if line.strip()] 385 + # 20231114 has 4 records 386 + assert len(lines) == 4 387 + # All should be from 20231114 388 + for line in lines: 389 + assert "1700000" in line # all agent_ids from that day start with 1700000 390 + 391 + 392 + def test_logs_runs_day_filter_no_match(capsys): 393 + """--day with nonexistent day produces empty output.""" 394 + logs_runs(day="20990101") 395 + output = capsys.readouterr().out 396 + assert output.strip() == "" 397 + 398 + 399 + def test_logs_runs_day_invalid(capsys): 400 + """--day with invalid format prints error.""" 401 + with pytest.raises(SystemExit): 402 + logs_runs(day="bad") 403 + output = capsys.readouterr().err 404 + assert "invalid --day format" in output.lower() 405 + 406 + 407 + def test_logs_runs_errors_filter(capsys): 408 + """--errors shows only error runs.""" 409 + logs_runs(errors=True) 410 + output = capsys.readouterr().out 411 + lines = [line for line in output.strip().splitlines() if line.strip()] 412 + # Only flow on 20231114 has status "error" 413 + assert len(lines) == 1 414 + assert "flow" in lines[0] 415 + assert "✗" in lines[0] 416 + 417 + 418 + def test_logs_runs_daily_filter(capsys): 419 + """--daily shows only daily-scheduled runs.""" 420 + logs_runs(daily=True) 421 + output = capsys.readouterr().out 422 + lines = [line for line in output.strip().splitlines() if line.strip()] 423 + # Daily runs: entities (20231113, schedule=daily), default x2 (20231114, 424 + # schedule=daily + legacy fallback) 425 + # Should NOT include flow (segment) or activity 426 + assert "flow" not in output 427 + assert "activity" not in output 428 + for line in lines: 429 + assert any(name in line for name in ["default", "entities"]) 430 + 431 + 432 + def test_logs_runs_daily_bumps_count(capsys): 433 + """--daily bumps default count to 50.""" 434 + # With only 6 total records in fixtures, verify explicit count still applies. 435 + logs_runs(daily=True, count=1) 436 + output = capsys.readouterr().out 437 + lines = [line for line in output.strip().splitlines() if line.strip()] 438 + assert len(lines) == 1 439 + 440 + 441 + def test_logs_runs_filter_composition(capsys): 442 + """Filters compose with AND logic.""" 443 + logs_runs(day="20231114", errors=True) 444 + output = capsys.readouterr().out 445 + lines = [line for line in output.strip().splitlines() if line.strip()] 446 + # Only flow on 20231114 is an error 447 + assert len(lines) == 1 448 + assert "flow" in lines[0] 449 + 450 + 451 + def test_logs_runs_summary(capsys): 452 + """--summary shows grouped aggregation.""" 453 + logs_runs(summary=True) 454 + output = capsys.readouterr().out 455 + # Should have agent names 456 + assert "default" in output 457 + assert "flow" in output 458 + assert "entities" in output 459 + assert "activity" in output 460 + # Should have totals line 461 + assert "total" in output 462 + # Should show pass/fail symbols 463 + assert "✓" in output 464 + assert "✗" in output 465 + 466 + 467 + def test_logs_runs_daily_summary(capsys): 468 + """--daily --summary shows only daily runs in summary.""" 469 + logs_runs(daily=True, summary=True) 470 + output = capsys.readouterr().out 471 + # Only daily agents (entities, default) 472 + assert "flow" not in output 473 + assert "activity" not in output 474 + assert "default" in output 475 + assert "entities" in output 476 + assert "total" in output 477 + 478 + 380 479 def test_parse_run_stats(): 381 480 """Parse run stats extracts correct counts from fixture JSONL.""" 382 481 from pathlib import Path
+1
think/cortex.py
··· 640 640 "runtime_seconds": runtime_seconds, 641 641 "provider": request.get("provider"), 642 642 "model": request.get("model"), 643 + "schedule": request.get("schedule"), 643 644 } 644 645 645 646 day_index_path = self.agents_dir / f"{day}.jsonl"
+4
think/dream.py
··· 441 441 if stream: 442 442 env["SOL_STREAM"] = stream 443 443 request_config["env"] = env 444 + request_config["schedule"] = target_schedule 444 445 445 446 prompt = ( 446 447 "" ··· 509 510 if stream: 510 511 env["SOL_STREAM"] = stream 511 512 request_config["env"] = env 513 + request_config["schedule"] = target_schedule 512 514 513 515 prompt = ( 514 516 "" ··· 838 840 "SOL_ACTIVITY": activity_id, 839 841 }, 840 842 } 843 + request_config["schedule"] = "activity" 841 844 if is_generate: 842 845 request_config["output"] = output_format 843 846 if refresh: ··· 1005 1008 "refresh": True, 1006 1009 "env": env, 1007 1010 } 1011 + request_config["schedule"] = "segment" 1008 1012 if is_generate: 1009 1013 request_config["output"] = config.get("output", "md") 1010 1014
+106 -4
think/muse_cli.py
··· 821 821 return None 822 822 823 823 824 - def logs_runs(*, agent: str | None = None, count: int = 20) -> None: 824 + def _print_summary(records: list[dict[str, Any]]) -> None: 825 + """Print grouped summary of agent runs.""" 826 + from collections import defaultdict 827 + 828 + groups: dict[str, list[dict[str, Any]]] = defaultdict(list) 829 + for r in records: 830 + groups[r.get("name", "unknown")].append(r) 831 + 832 + total_pass = 0 833 + total_fail = 0 834 + total_runtime = 0.0 835 + 836 + for name in sorted(groups): 837 + runs = groups[name] 838 + passed = sum(1 for r in runs if r.get("status") == "completed") 839 + failed = len(runs) - passed 840 + runtimes = [r.get("runtime_seconds") or 0 for r in runs] 841 + min_rt = min(runtimes) 842 + max_rt = max(runtimes) 843 + total_rt = sum(runtimes) 844 + 845 + total_pass += passed 846 + total_fail += failed 847 + total_runtime += total_rt 848 + 849 + if min_rt == max_rt: 850 + rt_str = f"{min_rt:.1f}s" 851 + else: 852 + rt_str = f"{min_rt:.1f}s–{max_rt:.1f}s" 853 + 854 + status_str = f"{passed}✓" 855 + if failed: 856 + status_str += f" {failed}✗" 857 + 858 + print(f" {name:<20} {status_str:<10} {rt_str}") 859 + 860 + print(f" {'—' * 40}") 861 + status_str = f"{total_pass}✓" 862 + if total_fail: 863 + status_str += f" {total_fail}✗" 864 + print(f" {'total':<20} {status_str:<10} {total_runtime:.1f}s") 865 + 866 + 867 + def logs_runs( 868 + *, 869 + agent: str | None = None, 870 + count: int | None = None, 871 + day: str | None = None, 872 + daily: bool = False, 873 + errors: bool = False, 874 + summary: bool = False, 875 + ) -> None: 825 876 """Print one-line summaries of recent agent runs from day-index files.""" 826 877 from think.models import calc_agent_cost 827 878 from think.utils import get_journal ··· 831 882 if not agents_dir.is_dir(): 832 883 return 833 884 885 + # Validate --day format 886 + if day and (len(day) != 8 or not day.isdigit()): 887 + print(f"Invalid --day format: {day}. Expected YYYYMMDD.", file=sys.stderr) 888 + sys.exit(1) 889 + 890 + # Resolve default count: 50 for --daily, 20 otherwise 891 + if count is None: 892 + count = 50 if daily else 20 893 + 834 894 # Find day-index files, most recent first 835 - day_files = sorted(agents_dir.glob("????????.jsonl"), reverse=True) 895 + if day: 896 + day_file = agents_dir / f"{day}.jsonl" 897 + day_files = [day_file] if day_file.is_file() else [] 898 + else: 899 + day_files = sorted(agents_dir.glob("????????.jsonl"), reverse=True) 836 900 if not day_files: 837 901 return 838 902 839 903 # Collect records across day files 840 904 records: list[dict[str, Any]] = [] 905 + _schedule_lookup: dict[str, str | None] | None = None 841 906 for day_file in day_files: 842 907 for line in day_file.read_text().splitlines(): 843 908 if not line.strip(): ··· 848 913 continue 849 914 if agent and record.get("name") != agent: 850 915 continue 916 + if errors and record.get("status") != "error": 917 + continue 918 + if daily: 919 + rec_schedule = record.get("schedule") 920 + if rec_schedule is None: 921 + if _schedule_lookup is None: 922 + all_configs = get_muse_configs(include_disabled=True) 923 + _schedule_lookup = { 924 + key: info.get("schedule") 925 + for key, info in all_configs.items() 926 + } 927 + rec_schedule = _schedule_lookup.get(record.get("name")) 928 + if rec_schedule != "daily": 929 + continue 851 930 records.append(record) 852 931 if len(records) >= count: 853 932 break ··· 858 937 # Sort by timestamp descending and trim 859 938 records.sort(key=lambda r: r.get("ts", 0), reverse=True) 860 939 records = records[:count] 940 + 941 + if summary: 942 + _print_summary(records) 943 + return 861 944 862 945 # Compute column widths 863 946 name_width = max((len(r.get("name", "")) for r in records), default=10) ··· 1086 1169 "-c", 1087 1170 "--count", 1088 1171 type=int, 1089 - default=20, 1172 + default=None, 1090 1173 help="Number of runs to show (default: 20)", 1174 + ) 1175 + logs_parser.add_argument( 1176 + "--day", metavar="YYYYMMDD", help="Show only runs from this day" 1177 + ) 1178 + logs_parser.add_argument( 1179 + "--daily", action="store_true", help="Show only daily-scheduled runs" 1180 + ) 1181 + logs_parser.add_argument( 1182 + "--errors", action="store_true", help="Show only error runs" 1183 + ) 1184 + logs_parser.add_argument( 1185 + "--summary", action="store_true", help="Show grouped summary" 1091 1186 ) 1092 1187 1093 1188 # --- log subcommand --- ··· 1114 1209 else: 1115 1210 show_prompt(args.name, as_json=args.json) 1116 1211 elif args.subcommand == "logs": 1117 - logs_runs(agent=args.agent, count=args.count) 1212 + logs_runs( 1213 + agent=args.agent, 1214 + count=args.count, 1215 + day=args.day, 1216 + daily=args.daily, 1217 + errors=args.errors, 1218 + summary=args.summary, 1219 + ) 1118 1220 elif args.subcommand == "log": 1119 1221 log_run(args.id, json_mode=args.json_mode, full=args.full) 1120 1222 elif args.subcommand == "list" and args.json: