personal memory agent
0
fork

Configure Feed

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

Add support settings UI and first-day check-in generator

Settings app: Support section with 4 fields — enabled toggle, proactive
detection toggle, anonymous feedback toggle, and portal URL text input.
Uses existing auto-save pattern (data-section/data-key attributes).
Defaults added to journal_default.json, allowed_sections in routes.py.

First-day check-in: segment-scheduled generator with pre-hook guard.
Zero-cost skip when onboarding isn't complete or <1 hour has elapsed.
One-shot — records firstday_checkin_sent in awareness state to prevent
repeat. Post-hook spawns a support agent chat with a warm check-in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+196
+1
apps/settings/routes.py
··· 102 102 ], 103 103 "transcribe": ["backend", "enrich", "preserve_all", "noise_upgrade"], 104 104 "convey": ["password"], 105 + "support": ["enabled", "proactive", "anonymous_feedback", "portal_url"], 105 106 "env": API_KEY_ENV_VARS, 106 107 } 107 108
+53
apps/settings/workspace.html
··· 1554 1554 <button class="settings-nav-item" data-section="insights">Insights</button> 1555 1555 <button class="settings-nav-item" data-section="security">Security</button> 1556 1556 <button class="settings-nav-item" data-section="sync">Sync</button> 1557 + <button class="settings-nav-item" data-section="support">Support</button> 1557 1558 </div> 1558 1559 <div class="settings-nav-group" id="facetNavGroup" style="display:none"> 1559 1560 <div class="settings-nav-label" id="facetNavLabel">Facet</div> ··· 2111 2112 </div> 2112 2113 </section> 2113 2114 2115 + <!-- Support Section --> 2116 + <section class="settings-section" id="section-support"> 2117 + <h2>Support</h2> 2118 + <p class="settings-section-desc">Configure how your sol communicates with sol pbc support.</p> 2119 + 2120 + <div class="settings-field"> 2121 + <div class="toggle-container" style="justify-content: flex-start; gap: 1em;"> 2122 + <label class="toggle-switch toggle-positive"> 2123 + <input type="checkbox" id="field-support-enabled" data-section="support" data-key="enabled"> 2124 + <span class="slider"></span> 2125 + </label> 2126 + <label for="field-support-enabled" class="toggle-label" style="cursor: pointer;">Support Agent</label> 2127 + </div> 2128 + <small>When enabled, your sol can file and track support tickets on your behalf. You always review and approve before anything is sent.</small> 2129 + </div> 2130 + 2131 + <div class="settings-field"> 2132 + <div class="toggle-container" style="justify-content: flex-start; gap: 1em;"> 2133 + <label class="toggle-switch toggle-positive"> 2134 + <input type="checkbox" id="field-support-proactive" data-section="support" data-key="proactive"> 2135 + <span class="slider"></span> 2136 + </label> 2137 + <label for="field-support-proactive" class="toggle-label" style="cursor: pointer;">Proactive Detection</label> 2138 + </div> 2139 + <small>Detect repeated errors and suggest filing a support ticket. No data is sent — only a local notification.</small> 2140 + </div> 2141 + 2142 + <div class="settings-field"> 2143 + <div class="toggle-container" style="justify-content: flex-start; gap: 1em;"> 2144 + <label class="toggle-switch toggle-positive"> 2145 + <input type="checkbox" id="field-support-anonymous" data-section="support" data-key="anonymous_feedback"> 2146 + <span class="slider"></span> 2147 + </label> 2148 + <label for="field-support-anonymous" class="toggle-label" style="cursor: pointer;">Anonymous Feedback</label> 2149 + </div> 2150 + <small>Strip installation identifiers when submitting feedback.</small> 2151 + </div> 2152 + 2153 + <div class="settings-field"> 2154 + <label for="field-support-portal-url">Portal URL</label> 2155 + <input type="text" id="field-support-portal-url" data-section="support" data-key="portal_url" placeholder="https://support.solpbc.org"> 2156 + <small>Support portal endpoint. Change this if you run your own portal.</small> 2157 + </div> 2158 + </section> 2159 + 2114 2160 <!-- Facet Appearance Section --> 2115 2161 <section class="settings-section" id="section-facet-appearance" data-requires-facet> 2116 2162 <div class="facet-header"> ··· 2447 2493 // Convey (password) 2448 2494 const convey = config.convey || {}; 2449 2495 setValue('field-password', convey.password || ''); 2496 + 2497 + // Support settings 2498 + const support = config.support || {}; 2499 + document.getElementById('field-support-enabled').checked = support.enabled !== false; 2500 + document.getElementById('field-support-proactive').checked = support.proactive !== false; 2501 + document.getElementById('field-support-anonymous').checked = support.anonymous_feedback || false; 2502 + setValue('field-support-portal-url', support.portal_url || 'https://support.solpbc.org'); 2450 2503 2451 2504 // Env (API keys) - show status indicators 2452 2505 // env = journal config status, system_env = shell/.env status
+17
muse/firstday_checkin.md
··· 1 + { 2 + "type": "generate", 3 + "title": "First-Day Check-In", 4 + "description": "One-shot check-in after onboarding — spawns support agent chat", 5 + "schedule": "segment", 6 + "priority": 98, 7 + "output": "text", 8 + "hook": {"pre": "firstday_checkin", "post": "firstday_checkin"}, 9 + "tier": 3, 10 + "thinking_budget": 512, 11 + "max_output_tokens": 256, 12 + "exclude_streams": ["import.*"] 13 + } 14 + 15 + This generator exists only to trigger the first-day check-in via its pre/post hooks. The pre-hook handles all logic — if it doesn't skip, the post-hook spawns a support agent chat. The LLM output is unused. 16 + 17 + Output "ok" — nothing else needed.
+119
muse/firstday_checkin.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """First-day check-in hooks. 5 + 6 + Pre-hook: guards on awareness state — skips immediately (zero API cost) 7 + unless onboarding is complete and ~1 hour has elapsed since completion. 8 + One-shot: once the check-in fires, it never fires again. 9 + 10 + Post-hook: records that the check-in was sent and spawns a support 11 + agent chat for the user. 12 + """ 13 + 14 + from __future__ import annotations 15 + 16 + import logging 17 + from datetime import datetime 18 + 19 + logger = logging.getLogger(__name__) 20 + 21 + # Minimum hours after onboarding completion before check-in fires 22 + MIN_HOURS_AFTER_COMPLETE = 1.0 23 + 24 + 25 + def pre_process(context: dict) -> dict | None: 26 + """Guard: skip unless onboarding is complete and 1+ hour has elapsed. 27 + 28 + Returns dict with skip_reason to skip, or None to proceed. 29 + """ 30 + from think.awareness import get_onboarding 31 + 32 + onboarding = get_onboarding() 33 + status = onboarding.get("status") 34 + 35 + # Only fire after onboarding completes 36 + if status != "complete": 37 + return {"skip_reason": "not_complete"} 38 + 39 + # Only fire once 40 + if onboarding.get("firstday_checkin_sent"): 41 + return {"skip_reason": "already_sent"} 42 + 43 + # Check elapsed time since onboarding started 44 + # (onboarding.started is when they began, but we want time since completion; 45 + # since complete_onboarding() doesn't record a timestamp, use started + a 46 + # generous window — 1 hour after they started is a reasonable proxy for 47 + # "settled in") 48 + started = onboarding.get("started", "") 49 + if not started: 50 + return {"skip_reason": "no_start_time"} 51 + 52 + hours = _elapsed_hours(started) 53 + if hours < MIN_HOURS_AFTER_COMPLETE: 54 + return {"skip_reason": "too_soon"} 55 + 56 + # All conditions met — proceed 57 + return None 58 + 59 + 60 + def post_process(result: str, context: dict) -> str | None: 61 + """Record check-in and spawn support agent chat.""" 62 + from think.awareness import append_log, update_state 63 + 64 + # Record that we sent it (prevents repeat) 65 + update_state("onboarding", {"firstday_checkin_sent": _now_iso()}) 66 + append_log( 67 + "state", 68 + key="onboarding.firstday_checkin_sent", 69 + message="First-day check-in sent to user", 70 + ) 71 + 72 + # Spawn support agent chat with check-in prompt 73 + try: 74 + from apps.utils import get_app_storage_path 75 + from convey.utils import save_json 76 + from think.callosum import callosum_send 77 + from think.cortex_client import cortex_request 78 + from think.utils import now_ms 79 + 80 + prompt = ( 81 + "The user recently completed onboarding. This is your first-day " 82 + "check-in. Send a warm, brief message: ask how things are going, " 83 + "if anything is surprising or confusing, and remind them you're " 84 + "here to help or capture feedback anytime. Keep it short and " 85 + "conversational — one message, not a wall of text." 86 + ) 87 + agent_id = cortex_request(prompt=prompt, name="support") 88 + if agent_id: 89 + chat_record = { 90 + "ts": now_ms(), 91 + "muse": "support:support", 92 + "title": "Check-in", 93 + "agent_ids": [agent_id], 94 + } 95 + chats_dir = get_app_storage_path("chat", "chats") 96 + save_json(chats_dir / f"{agent_id}.json", chat_record) 97 + callosum_send("navigate", "request", path=f"/app/chat#{agent_id}") 98 + logger.info("Sent first-day check-in redirect: %s", agent_id) 99 + except Exception: 100 + logger.exception("Failed to send first-day check-in") 101 + 102 + return result 103 + 104 + 105 + def _elapsed_hours(started_iso: str) -> float: 106 + """Calculate hours elapsed since the started timestamp.""" 107 + if not started_iso: 108 + return 0.0 109 + try: 110 + start = datetime.strptime(started_iso, "%Y%m%dT%H:%M:%S") 111 + elapsed = (datetime.now() - start).total_seconds() 112 + return elapsed / 3600 113 + except (ValueError, TypeError): 114 + return 0.0 115 + 116 + 117 + def _now_iso() -> str: 118 + """Return current time as compact ISO string.""" 119 + return datetime.now().strftime("%Y%m%dT%H:%M:%S")
+6
think/journal_default.json
··· 13 13 "email_addresses": [], 14 14 "timezone": "" 15 15 }, 16 + "support": { 17 + "enabled": true, 18 + "proactive": true, 19 + "anonymous_feedback": false, 20 + "portal_url": "https://support.solpbc.org" 21 + }, 16 22 "describe": { 17 23 "redact": [ 18 24 "use *** instead of any visible passwords, credentials, keys, tokens, and secrets",