personal memory agent
0
fork

Configure Feed

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

activity_state: add active_entities field to activity output

Prompt now asks LLM to include an active_entities array of people,
companies, projects, and tools noticeably active per activity.
Post-hook passes through on active items, omits on ended. Bumped
max_output_tokens to 1024 to accommodate.

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

+132 -31
+7 -6
muse/activity_state.md
··· 10 10 "hook": {"pre": "activity_state", "post": "activity_state"}, 11 11 "tier": 3, 12 12 "thinking_budget": 2048, 13 - "max_output_tokens": 512, 13 + "max_output_tokens": 1024, 14 14 "instructions": { 15 15 "sources": {"audio": true, "screen": true, "agents": false}, 16 16 "facets": false ··· 40 40 41 41 ```json 42 42 [ 43 - {"activity": "meeting", "state": "continuing", "description": "Design review with UX team, now discussing navigation", "level": "high"}, 44 - {"activity": "messaging", "state": "new", "description": "Slack thread about deployment", "level": "low"}, 43 + {"activity": "meeting", "state": "continuing", "description": "Design review with UX team, now discussing navigation", "level": "high", "active_entities": ["Sarah Chen", "UX Team"]}, 44 + {"activity": "messaging", "state": "new", "description": "Slack thread about deployment", "level": "low", "active_entities": ["DevOps"]}, 45 45 {"activity": "email", "state": "ended", "description": "Replied to deployment notification from ops team"} 46 46 ] 47 47 ``` ··· 52 52 - `state`: One of `"continuing"`, `"new"`, or `"ended"` 53 53 - `description`: Brief description of what this activity involves (update as context evolves) 54 54 - `level`: Engagement level — `"high"` (primary focus), `"medium"` (secondary), `"low"` (background). Only for continuing/new, omit for ended. 55 + - `active_entities`: Names of people, companies, projects, or tools that were noticeably active in this segment and associated with this activity. Only include entities with clear evidence of involvement (speaking, mentioned, visible on screen). Omit for ended. 55 56 56 57 ## Rules 57 58 ··· 66 67 67 68 **New activity starts:** 68 69 ```json 69 - [{"activity": "coding", "state": "new", "description": "Implementing user auth flow", "level": "high"}] 70 + [{"activity": "coding", "state": "new", "description": "Implementing user auth flow", "level": "high", "active_entities": ["Claude Code", "VS Code"]}] 70 71 ``` 71 72 72 73 **Activity continues from previous:** 73 74 ```json 74 - [{"activity": "meeting", "state": "continuing", "description": "Sprint planning - now discussing blockers", "level": "high"}] 75 + [{"activity": "meeting", "state": "continuing", "description": "Sprint planning - now discussing blockers", "level": "high", "active_entities": ["Alice Johnson", "Bob Smith"]}] 75 76 ``` 76 77 77 78 **One meeting ends, another starts:** 78 79 ```json 79 80 [ 80 81 {"activity": "meeting", "state": "ended", "description": "Sprint planning completed"}, 81 - {"activity": "meeting", "state": "new", "description": "1:1 with manager", "level": "high"} 82 + {"activity": "meeting", "state": "new", "description": "1:1 with manager", "level": "high", "active_entities": ["Manager Name"]} 82 83 ] 83 84 ``` 84 85
+30 -25
muse/activity_state.py
··· 499 499 # Build unclaimed candidates with their original indices 500 500 unclaimed = [(i, c) for i, c in enumerate(prev_active) if i not in claimed] 501 501 502 + active_entities = item.get("active_entities", []) 503 + 502 504 if state == "continuing": 503 505 result = _find_best_match(activity_id, description, unclaimed) 504 506 if result: ··· 509 511 # No previous match — treat as new 510 512 since = segment 511 513 512 - resolved.append( 513 - { 514 - "activity": activity_id, 515 - "state": "active", 516 - "since": since, 517 - "description": description, 518 - "level": item.get("level", "medium"), 519 - } 520 - ) 514 + entry = { 515 + "activity": activity_id, 516 + "state": "active", 517 + "since": since, 518 + "description": description, 519 + "level": item.get("level", "medium"), 520 + } 521 + if active_entities: 522 + entry["active_entities"] = active_entities 523 + resolved.append(entry) 521 524 522 525 elif state == "ended": 523 526 result = _find_best_match(activity_id, description, unclaimed) ··· 537 540 ): 538 541 # No active match but has a novel description — likely 539 542 # a real activity the LLM mis-tagged as ended; treat as new 540 - resolved.append( 541 - { 542 - "activity": activity_id, 543 - "state": "active", 544 - "since": segment, 545 - "description": description, 546 - "level": item.get("level", "medium"), 547 - } 548 - ) 549 - # else: redundant re-report of already ended activity — drop 550 - 551 - else: 552 - # "new" or any unrecognized state — stamp current segment 553 - resolved.append( 554 - { 543 + entry = { 555 544 "activity": activity_id, 556 545 "state": "active", 557 546 "since": segment, 558 547 "description": description, 559 548 "level": item.get("level", "medium"), 560 549 } 561 - ) 550 + if active_entities: 551 + entry["active_entities"] = active_entities 552 + resolved.append(entry) 553 + # else: redundant re-report of already ended activity — drop 554 + 555 + else: 556 + # "new" or any unrecognized state — stamp current segment 557 + entry = { 558 + "activity": activity_id, 559 + "state": "active", 560 + "since": segment, 561 + "description": description, 562 + "level": item.get("level", "medium"), 563 + } 564 + if active_entities: 565 + entry["active_entities"] = active_entities 566 + resolved.append(entry) 562 567 563 568 return json.dumps(resolved, ensure_ascii=False)
+95
tests/test_activity_state.py
··· 835 835 items = json.loads(result) 836 836 assert items[0]["level"] == "medium" 837 837 838 + def test_active_entities_passthrough_on_new(self): 839 + """active_entities array is passed through on new activities.""" 840 + from muse.activity_state import post_process 841 + 842 + llm_output = json.dumps( 843 + [ 844 + { 845 + "activity": "meeting", 846 + "state": "new", 847 + "description": "Standup with team", 848 + "level": "high", 849 + "active_entities": ["Alice", "Bob"], 850 + } 851 + ] 852 + ) 853 + 854 + result = post_process(llm_output, {"segment": "143000_300"}) 855 + items = json.loads(result) 856 + assert items[0]["active_entities"] == ["Alice", "Bob"] 857 + 858 + def test_active_entities_omitted_when_empty(self): 859 + """active_entities is omitted from output when not provided or empty.""" 860 + from muse.activity_state import post_process 861 + 862 + llm_output = json.dumps( 863 + [ 864 + { 865 + "activity": "coding", 866 + "state": "new", 867 + "description": "Writing code", 868 + "level": "high", 869 + "active_entities": [], 870 + } 871 + ] 872 + ) 873 + 874 + result = post_process(llm_output, {"segment": "143000_300"}) 875 + items = json.loads(result) 876 + assert "active_entities" not in items[0] 877 + 878 + def test_active_entities_omitted_on_ended(self): 879 + """active_entities is not included on ended activities.""" 880 + from muse.activity_state import post_process 881 + 882 + with tempfile.TemporaryDirectory() as tmpdir: 883 + original_path = os.environ.get("JOURNAL_PATH") 884 + os.environ["JOURNAL_PATH"] = tmpdir 885 + 886 + try: 887 + day_dir = Path(tmpdir) / "20260130" 888 + day_dir.mkdir() 889 + 890 + prev_dir = day_dir / "100000_300" 891 + prev_dir.mkdir() 892 + prev_state = [ 893 + { 894 + "activity": "meeting", 895 + "state": "active", 896 + "since": "093000_300", 897 + "description": "Sprint planning", 898 + "level": "high", 899 + } 900 + ] 901 + (prev_dir / "activity_state_work.json").write_text( 902 + json.dumps(prev_state) 903 + ) 904 + 905 + (day_dir / "100500_300").mkdir() 906 + 907 + llm_output = json.dumps( 908 + [ 909 + { 910 + "activity": "meeting", 911 + "state": "ended", 912 + "description": "Sprint planning completed", 913 + "active_entities": ["Alice"], 914 + } 915 + ] 916 + ) 917 + 918 + context = { 919 + "day": "20260130", 920 + "segment": "100500_300", 921 + "output_path": f"{tmpdir}/20260130/100500_300/activity_state_work.json", 922 + } 923 + 924 + result = post_process(llm_output, context) 925 + items = json.loads(result) 926 + assert items[0]["state"] == "ended" 927 + assert "active_entities" not in items[0] 928 + 929 + finally: 930 + if original_path: 931 + os.environ["JOURNAL_PATH"] = original_path 932 + 838 933 def test_fuzzy_match_disambiguates_same_type(self): 839 934 """Multiple same-type previous activities matched by description.""" 840 935 from muse.activity_state import post_process