personal memory agent
0
fork

Configure Feed

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

Post-onboarding discovery: evolving placeholder, triage context, first_daily_ready flag

Make daily agent outputs visible after onboarding completes. Chat bar placeholder
now cascades through journal maturity states instead of falling to "Send a message..."
for complete/skipped users. Triage injects available daily analysis names into agent
context. Dream pipeline sets journal.first_daily_ready awareness flag after first
post-onboarding daily run. Onboarding and observation review prompts now reference
specific entities in their closing messages.

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

+260 -15
+30 -11
convey/apps.py
··· 68 68 return cookie_facet if cookie_facet is not None else config_facet 69 69 70 70 71 + def _resolve_placeholder( 72 + onboarding_status: str, awareness_current: dict, day_count: int 73 + ) -> str: 74 + """Resolve chat bar placeholder text based on journal state.""" 75 + if onboarding_status == "observing": 76 + return "I'm learning how you work — ask me what I've noticed..." 77 + if onboarding_status == "ready": 78 + return "I have suggestions for organizing your journal — let's review" 79 + if onboarding_status == "interviewing": 80 + return "Tell me about your work..." 81 + if onboarding_status in ("complete", "skipped"): 82 + if awareness_current.get("journal", {}).get("first_daily_ready"): 83 + if day_count < 2: 84 + return "Your first daily analysis is ready — ask me what I found..." 85 + if day_count >= 7: 86 + return ( 87 + "Ask me about your day, search your journal, or explore insights..." 88 + ) 89 + return "Your daily analysis is ready — ask about today or anything in your journal..." 90 + return "Capture is running — your first daily analysis will be ready soon..." 91 + return "Send a message..." 92 + 93 + 71 94 def register_app_context(app: Flask, registry: AppRegistry) -> None: 72 95 """Register app system context processors and template filters.""" 73 96 from .utils import DATE_RE, format_date_short ··· 129 152 # Chat bar placeholder based on onboarding state 130 153 chat_bar_placeholder = "Send a message..." 131 154 try: 132 - from think.awareness import get_onboarding 155 + from think.awareness import get_current, get_onboarding 156 + from think.utils import day_dirs 133 157 134 158 onboarding = get_onboarding() 135 159 onboarding_status = onboarding.get("status", "") 136 - if onboarding_status == "observing": 137 - chat_bar_placeholder = ( 138 - "I'm learning how you work — ask me what I've noticed..." 139 - ) 140 - elif onboarding_status == "ready": 141 - chat_bar_placeholder = ( 142 - "I have suggestions for organizing your journal — let's review" 143 - ) 144 - elif onboarding_status == "interviewing": 145 - chat_bar_placeholder = "Tell me about your work..." 160 + awareness_current = get_current() 161 + day_count = len(day_dirs()) 162 + chat_bar_placeholder = _resolve_placeholder( 163 + onboarding_status, awareness_current, day_count 164 + ) 146 165 except Exception: 147 166 pass # Default placeholder on any error 148 167
+34
convey/triage.py
··· 76 76 "Suggest the user review their recommendations. Use `sol call chat redirect` " 77 77 "to open a chat with the recommendation agent if they want to proceed." 78 78 ) 79 + elif onboarding_status in ("complete", "skipped"): 80 + # Add daily agent output context for post-onboarding users 81 + try: 82 + from datetime import datetime, timedelta 83 + from pathlib import Path 84 + 85 + from think.utils import get_journal 86 + 87 + journal = Path(get_journal()) 88 + today = datetime.now().strftime("%Y%m%d") 89 + relevant_day = today 90 + agents_dir = journal / today / "agents" 91 + outputs = ( 92 + sorted(p.stem for p in agents_dir.glob("*.md")) 93 + if agents_dir.is_dir() 94 + else [] 95 + ) 96 + if not outputs: 97 + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d") 98 + agents_dir = journal / yesterday / "agents" 99 + outputs = ( 100 + sorted(p.stem for p in agents_dir.glob("*.md")) 101 + if agents_dir.is_dir() 102 + else [] 103 + ) 104 + relevant_day = yesterday 105 + if outputs: 106 + names = ", ".join(outputs) 107 + context_lines.append( 108 + f"Daily analysis available: {names} (from {relevant_day}). " 109 + "The user can ask about any of these topics." 110 + ) 111 + except Exception: 112 + pass # Don't let context enrichment break triage 79 113 80 114 if context_lines: 81 115 full_prompt = "\n".join(context_lines) + "\n\n" + message
+1 -1
muse/observation_review.md
··· 88 88 sol call journal facets 89 89 ``` 90 90 91 - Tell the user their journal is now set up and the system will start organizing captures into these facets. They can always adjust facets later. 91 + Tell the user their journal is now set up and the system will start organizing captures into these facets. Reference the specific entities you just created or attached — name them — and suggest a first thing to try: pick one entity and say something like "Try asking me 'tell me about [entity name]' — I'll pull together everything I know." They can always adjust facets and entities later. 92 92 93 93 ## Behavioral Rules 94 94
+1 -1
muse/onboarding.md
··· 81 81 4. Ask what entities belong in each facet. 82 82 5. Attach each via `sol call entities attach`. 83 83 6. Run `sol call awareness onboarding --complete`. 84 - 7. Confirm setup completion and handoff to normal mode. 84 + 7. Summarize what was created — name the specific facets and entities you just set up. Then suggest a concrete first thing to try: pick one of the entities you just attached and say something like "Try asking me 'tell me about [entity name]' to see how I can help." Keep it warm and grounded in what was just created together.
+28
tests/test_awareness.py
··· 301 301 assert result.exit_code == 0 302 302 data = json.loads(result.output) 303 303 assert data["data"]["meetings"] == 2 304 + 305 + 306 + class TestJournalState: 307 + def test_first_daily_ready_via_update_state(self): 308 + from think.awareness import get_current, update_state 309 + 310 + update_state( 311 + "journal", 312 + {"first_daily_ready": True, "first_daily_ready_at": "20260308T14:00:00"}, 313 + ) 314 + 315 + state = get_current() 316 + assert state["journal"]["first_daily_ready"] is True 317 + assert state["journal"]["first_daily_ready_at"] == "20260308T14:00:00" 318 + 319 + def test_first_daily_ready_preserves_onboarding(self): 320 + from think.awareness import get_current, update_state 321 + 322 + update_state("onboarding", {"status": "complete", "path": "b"}) 323 + update_state( 324 + "journal", 325 + {"first_daily_ready": True, "first_daily_ready_at": "20260308T14:00:00"}, 326 + ) 327 + 328 + state = get_current() 329 + assert state["onboarding"]["status"] == "complete" 330 + assert state["onboarding"]["path"] == "b" 331 + assert state["journal"]["first_daily_ready"] is True
+2 -2
tests/test_observation.py
··· 333 333 start_onboarding("a") 334 334 update_state("onboarding", {"status": "complete"}) 335 335 placeholder = self._get_placeholder() 336 - assert placeholder == "Send a message..." 336 + assert "Capture is running" in placeholder 337 337 338 338 def test_skipped_placeholder(self): 339 339 from think.awareness import skip_onboarding 340 340 341 341 skip_onboarding() 342 342 placeholder = self._get_placeholder() 343 - assert placeholder == "Send a message..." 343 + assert "Capture is running" in placeholder 344 344 345 345 346 346 class TestChatRedirectMuse:
+144
tests/test_onboarding.py
··· 170 170 ) 171 171 mock_request = _run_chat_cli_main(args, facets={}, onboarding={"status": "skipped"}) 172 172 assert mock_request.call_args.kwargs["name"] == "default" 173 + 174 + 175 + # --- Placeholder resolution --- 176 + 177 + 178 + class TestPlaceholderResolution: 179 + def test_observing(self): 180 + from convey.apps import _resolve_placeholder 181 + 182 + result = _resolve_placeholder("observing", {}, 0) 183 + assert "learning how you work" in result 184 + 185 + def test_ready(self): 186 + from convey.apps import _resolve_placeholder 187 + 188 + result = _resolve_placeholder("ready", {}, 0) 189 + assert "suggestions" in result 190 + 191 + def test_interviewing(self): 192 + from convey.apps import _resolve_placeholder 193 + 194 + result = _resolve_placeholder("interviewing", {}, 0) 195 + assert "Tell me about" in result 196 + 197 + def test_complete_no_daily(self): 198 + from convey.apps import _resolve_placeholder 199 + 200 + result = _resolve_placeholder("complete", {}, 0) 201 + assert "Capture is running" in result 202 + 203 + def test_complete_first_daily_young(self): 204 + from convey.apps import _resolve_placeholder 205 + 206 + current = {"journal": {"first_daily_ready": True}} 207 + result = _resolve_placeholder("complete", current, 1) 208 + assert "first daily analysis is ready" in result 209 + 210 + def test_complete_first_daily_mid(self): 211 + from convey.apps import _resolve_placeholder 212 + 213 + current = {"journal": {"first_daily_ready": True}} 214 + result = _resolve_placeholder("complete", current, 3) 215 + assert "daily analysis is ready" in result 216 + assert "first" not in result 217 + 218 + def test_complete_first_daily_mature(self): 219 + from convey.apps import _resolve_placeholder 220 + 221 + current = {"journal": {"first_daily_ready": True}} 222 + result = _resolve_placeholder("complete", current, 10) 223 + assert "Ask me about your day" in result 224 + 225 + def test_skipped_no_daily(self): 226 + from convey.apps import _resolve_placeholder 227 + 228 + result = _resolve_placeholder("skipped", {}, 0) 229 + assert "Capture is running" in result 230 + 231 + def test_skipped_with_daily_mature(self): 232 + from convey.apps import _resolve_placeholder 233 + 234 + current = {"journal": {"first_daily_ready": True}} 235 + result = _resolve_placeholder("skipped", current, 10) 236 + assert "Ask me about your day" in result 237 + 238 + def test_unknown_status_fallback(self): 239 + from convey.apps import _resolve_placeholder 240 + 241 + result = _resolve_placeholder("", {}, 0) 242 + assert result == "Send a message..." 243 + 244 + def test_no_status_fallback(self): 245 + from convey.apps import _resolve_placeholder 246 + 247 + result = _resolve_placeholder("", {}, 5) 248 + assert result == "Send a message..." 249 + 250 + 251 + # --- Triage daily output context --- 252 + 253 + 254 + class TestTriageDailyContext: 255 + def test_triage_complete_injects_daily_context(self, tmp_path): 256 + """When agent outputs exist, the prompt includes daily analysis context.""" 257 + from datetime import datetime 258 + 259 + today = datetime.now().strftime("%Y%m%d") 260 + agents_dir = tmp_path / today / "agents" 261 + agents_dir.mkdir(parents=True) 262 + (agents_dir / "flow.md").write_text("# Flow") 263 + (agents_dir / "meetings.md").write_text("# Meetings") 264 + 265 + mock = _run_triage( 266 + facets={"work": {}}, 267 + onboarding={"status": "complete"}, 268 + ) 269 + prompt = mock.call_args.kwargs["prompt"] 270 + assert "Daily analysis available" in prompt 271 + assert "flow" in prompt 272 + assert "meetings" in prompt 273 + 274 + def test_triage_complete_no_outputs_no_extra_context(self): 275 + """When no agent outputs exist, no daily analysis context is added.""" 276 + mock = _run_triage( 277 + facets={"work": {}}, 278 + onboarding={"status": "complete"}, 279 + ) 280 + prompt = mock.call_args.kwargs["prompt"] 281 + assert "Daily analysis" not in prompt 282 + 283 + def test_triage_complete_falls_back_to_yesterday(self, tmp_path): 284 + """When today has no outputs but yesterday does, use yesterday's.""" 285 + from datetime import datetime, timedelta 286 + 287 + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d") 288 + agents_dir = tmp_path / yesterday / "agents" 289 + agents_dir.mkdir(parents=True) 290 + (agents_dir / "flow.md").write_text("# Flow") 291 + 292 + mock = _run_triage( 293 + facets={"work": {}}, 294 + onboarding={"status": "complete"}, 295 + ) 296 + prompt = mock.call_args.kwargs["prompt"] 297 + assert "Daily analysis available" in prompt 298 + assert "flow" in prompt 299 + assert yesterday in prompt 300 + 301 + def test_triage_skipped_injects_daily_context(self, tmp_path): 302 + """Skipped onboarding also gets daily analysis context.""" 303 + from datetime import datetime 304 + 305 + today = datetime.now().strftime("%Y%m%d") 306 + agents_dir = tmp_path / today / "agents" 307 + agents_dir.mkdir(parents=True) 308 + (agents_dir / "knowledge_graph.md").write_text("# KG") 309 + 310 + mock = _run_triage( 311 + facets={}, 312 + onboarding={"status": "skipped"}, 313 + ) 314 + prompt = mock.call_args.kwargs["prompt"] 315 + assert "Daily analysis available" in prompt 316 + assert "knowledge_graph" in prompt
+20
think/dream.py
··· 1652 1652 except Exception: 1653 1653 pass 1654 1654 1655 + # Set first_daily_ready awareness flag after first post-onboarding daily 1656 + try: 1657 + from think.awareness import get_current, get_onboarding, update_state 1658 + 1659 + ob = get_onboarding() 1660 + if ob.get("status") == "complete": 1661 + cur = get_current() 1662 + if not cur.get("journal", {}).get("first_daily_ready"): 1663 + update_state( 1664 + "journal", 1665 + { 1666 + "first_daily_ready": True, 1667 + "first_daily_ready_at": datetime.now().strftime( 1668 + "%Y%m%dT%H:%M:%S" 1669 + ), 1670 + }, 1671 + ) 1672 + except Exception: 1673 + pass 1674 + 1655 1675 # Build log message 1656 1676 msg = "dream" 1657 1677 if args.refresh: