personal memory agent
0
fork

Configure Feed

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

init: add retention config step to setup wizard

Add a "your recordings" retention configuration step between observers
(step 4) and get started (step 6) in the first-time setup wizard.
Users choose between keep-forever, keep-N-days, and delete-after-
processing modes with day presets (7/30/90/custom).

finalize() includes retention_mode and retention_days in the POST
payload; init_finalize() writes them as retention.raw_media and
retention.raw_media_days to journal.json. Default is days/7 when
untouched.

Remove the hard-coded retention paragraph from the welcome screen
since users now configure retention during setup.

+105 -4
-1
apps/home/workspace.html
··· 953 953 <div class="pulse-welcome"> 954 954 <h2>welcome to your home page</h2> 955 955 <p>this is where your day comes together — narrative summaries, calendar events, tasks, routines, and the people in your network. as solstone captures and processes your day, sections will appear here automatically.</p> 956 - <p>your recordings are kept for 7 days by default, then cleaned up after processing. you can change this anytime in <a href="/app/settings#storage">settings</a>.</p> 957 956 <a href="/app/health">check system health →</a> 958 957 </div> 959 958 {% endif %}
+13
convey/root.py
··· 238 238 if gemini_key: 239 239 config.setdefault("env", {})["GOOGLE_API_KEY"] = gemini_key 240 240 config.setdefault("setup", {})["completed_at"] = now_ms() 241 + retention_mode = data.get("retention_mode", "days") 242 + retention_days = data.get("retention_days", 7) 243 + if isinstance(retention_days, str): 244 + try: 245 + retention_days = int(retention_days) 246 + except (ValueError, TypeError): 247 + retention_days = 7 248 + config.setdefault("retention", {}).update( 249 + { 250 + "raw_media": retention_mode, 251 + "raw_media_days": retention_days if retention_mode == "days" else None, 252 + } 253 + ) 241 254 242 255 config_path = Path(get_journal()) / "config" / "journal.json" 243 256 config_path.parent.mkdir(parents=True, exist_ok=True)
+64 -3
convey/templates/init.html
··· 59 59 .observer-empty { font-size: 0.85rem; color: #888; } 60 60 .observer-empty a { color: #b06a1a; } 61 61 .footer-note { font-size: 0.88rem; color: #767676; margin-top: 1.5rem; text-align: center; } 62 + .settings-mode-btn { 63 + padding: 0.5em 1em; border: 1px solid #ccc; border-radius: 4px; 64 + background: #fff; color: #555; cursor: pointer; font-size: 0.85rem; 65 + } 66 + .settings-mode-btn:hover { border-color: #999; } 67 + .settings-mode-btn:active:not(.active) { background: #f5f5f5; } 68 + .settings-mode-btn.active { background: #E8923A; color: #fff; border-color: #E8923A; } 69 + .settings-preset-btn { 70 + padding: 0.35em 0.75em; border: 1px solid #ccc; border-radius: 4px; 71 + background: #fff; color: #555; cursor: pointer; font-size: 0.85rem; 72 + } 73 + .settings-preset-btn:hover { border-color: #999; } 74 + .settings-preset-btn.active { border-color: #E8923A; color: #E8923A; } 75 + .retention-days-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; } 76 + .retention-days-row input[type="number"] { 77 + width: 5em; padding: 0.35em 0.5em; border: 2px solid #ddd; border-radius: 8px; 78 + font-size: 0.85rem; box-sizing: border-box; 79 + } 80 + .retention-days-row input[type="number"]:focus { outline: none; border-color: #E8923A; } 62 81 </style> 63 82 </head> 64 83 <body> ··· 131 150 </div> 132 151 </section> 133 152 153 + <section class="init-section disabled" id="section-recordings"> 154 + <h3>5. your recordings</h3> 155 + <p class="section-hint">choose how long to keep raw audio and screen recordings. you can change this anytime in settings.</p> 156 + <div class="field-group"> 157 + <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> 158 + <button type="button" class="settings-mode-btn active" data-mode="days" onclick="setRetentionMode('days')">keep N days</button> 159 + <button type="button" class="settings-mode-btn" data-mode="keep" onclick="setRetentionMode('keep')">keep forever</button> 160 + <button type="button" class="settings-mode-btn" data-mode="processed" onclick="setRetentionMode('processed')">delete after processing</button> 161 + </div> 162 + <div id="retention-days-field" class="retention-days-row"> 163 + <button type="button" class="settings-preset-btn active" data-days="7" onclick="setRetentionDays(7)">7</button> 164 + <button type="button" class="settings-preset-btn" data-days="30" onclick="setRetentionDays(30)">30</button> 165 + <button type="button" class="settings-preset-btn" data-days="90" onclick="setRetentionDays(90)">90</button> 166 + <input type="number" id="retention-days-input" min="1" max="365" value="7" onchange="setRetentionDaysCustom(this.value)"> 167 + <span style="font-size: 0.85rem; color: #666;">days</span> 168 + </div> 169 + </div> 170 + </section> 171 + 134 172 <section class="init-section disabled" id="section-finalize"> 135 - <h3>5. get started</h3> 173 + <h3>6. get started</h3> 136 174 <p class="section-hint">solstone works best with a coding agent. open a terminal, navigate to your solstone directory, and ask it to help you set up:</p> 137 175 <p class="section-hint"><code>cd {{ repo_path }} &amp;&amp; claude "help me set up solstone"</code></p> 138 176 <p class="section-hint"><code>cd {{ repo_path }} &amp;&amp; codex "help me set up solstone"</code></p> ··· 180 218 let detectedTimezone = ''; 181 219 182 220 function unlockSections() { 183 - ['section-identity', 'section-provider', 'section-observers', 'section-finalize'].forEach(id => { 221 + ['section-identity', 'section-provider', 'section-observers', 'section-recordings', 'section-finalize'].forEach(id => { 184 222 const section = document.getElementById(id); 185 223 section.classList.remove('disabled'); 186 224 section.querySelectorAll('input[disabled]').forEach(inp => inp.disabled = false); ··· 250 288 } catch (err) {} 251 289 } 252 290 291 + function setRetentionMode(mode) { 292 + document.querySelectorAll('#section-recordings .settings-mode-btn').forEach(btn => { 293 + btn.classList.toggle('active', btn.dataset.mode === mode); 294 + }); 295 + document.getElementById('retention-days-field').style.display = mode === 'days' ? 'flex' : 'none'; 296 + } 297 + 298 + function setRetentionDays(days) { 299 + document.querySelectorAll('#section-recordings .settings-preset-btn').forEach(btn => { 300 + btn.classList.toggle('active', parseInt(btn.dataset.days) === days); 301 + }); 302 + document.getElementById('retention-days-input').value = days; 303 + } 304 + 305 + function setRetentionDaysCustom(value) { 306 + const days = parseInt(value) || 7; 307 + document.querySelectorAll('#section-recordings .settings-preset-btn').forEach(btn => { 308 + btn.classList.toggle('active', parseInt(btn.dataset.days) === days); 309 + }); 310 + } 311 + 253 312 async function finalize() { 254 313 try { 255 314 const resp = await fetch('/init/finalize', { ··· 259 318 name: document.getElementById('name').value.trim(), 260 319 preferred: document.getElementById('preferred').value.trim(), 261 320 timezone: detectedTimezone, 262 - gemini_key: document.getElementById('gemini-key').value.trim() 321 + gemini_key: document.getElementById('gemini-key').value.trim(), 322 + retention_mode: (document.querySelector('#section-recordings .settings-mode-btn.active') || {}).dataset?.mode || 'days', 323 + retention_days: parseInt(document.getElementById('retention-days-input').value) || 7 263 324 }) 264 325 }); 265 326 const data = await resp.json();
+28
tests/test_init.py
··· 247 247 resp = fresh_client.get("/init") 248 248 assert resp.status_code == 302 249 249 250 + def test_finalize_with_retention_config(self, fresh_client, journal_copy): 251 + """Finalize with explicit retention config writes correct values.""" 252 + resp = fresh_client.post( 253 + "/init/finalize", 254 + json={ 255 + "password": "securepass123", 256 + "retention_mode": "processed", 257 + "retention_days": 30, 258 + }, 259 + content_type="application/json", 260 + ) 261 + assert resp.status_code == 200 262 + config = _read_config(journal_copy) 263 + assert config["retention"]["raw_media"] == "processed" 264 + assert config["retention"]["raw_media_days"] is None 265 + 266 + def test_finalize_default_retention(self, fresh_client, journal_copy): 267 + """Finalize without retention fields writes default (days/7).""" 268 + resp = fresh_client.post( 269 + "/init/finalize", 270 + json={"password": "securepass123"}, 271 + content_type="application/json", 272 + ) 273 + assert resp.status_code == 200 274 + config = _read_config(journal_copy) 275 + assert config["retention"]["raw_media"] == "days" 276 + assert config["retention"]["raw_media_days"] == 7 277 + 250 278 251 279 class TestRemovedEndpoints: 252 280 """Verify old endpoints no longer exist."""