···953953 <div class="pulse-welcome">
954954 <h2>welcome to your home page</h2>
955955 <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>
956956+ <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>
956957 <a href="/app/health">check system health →</a>
957958 </div>
958959 {% endif %}
+4-4
docs/JOURNAL.md
···153153154154The `retention` block controls automatic cleanup of layer 1 raw media (audio recordings, video captures, screen diffs) while preserving all layer 2 extracts and layer 3 agent outputs. Three modes control when raw media is deleted:
155155156156-- `"keep"` – retain raw media indefinitely (default)
157157-- `"days"` – delete raw media after `raw_media_days` days, once the segment has finished processing
156156+- `"keep"` – retain raw media indefinitely
157157+- `"days"` – delete raw media after `raw_media_days` days, once the segment has finished processing (default: 7 days)
158158- `"processed"` – delete raw media as soon as the segment has finished processing
159159160160```json
···176176```
177177178178Fields:
179179-- `raw_media` (string) – Retention mode: `"keep"`, `"days"`, or `"processed"`. Default: `"processed"`.
180180-- `raw_media_days` (integer or null) – Number of days to retain raw media when mode is `"days"`. Required when `raw_media` is `"days"`, ignored otherwise.
179179+- `raw_media` (string) – Retention mode: `"keep"`, `"days"`, or `"processed"`. Default: `"days"`.
180180+- `raw_media_days` (integer or null) – Number of days to retain raw media when mode is `"days"`. Default: `7`. Required when `raw_media` is `"days"`, ignored otherwise.
181181- `per_stream` (object) – Per-stream overrides keyed by stream name. Each entry supports `raw_media` and `raw_media_days`. Omitted fields inherit from the global retention settings.
182182183183"Raw media" means layer 1 capture files only: audio files (`.flac`, `.opus`, `.ogg`, `.m4a`, `.wav`), video files (`.webm`, `.mov`, `.mp4`), and screen diffs (`monitor_*_diff.png`).
···66Manages the lifecycle of raw media files (layer 1 captures) in journal segments.
77Three retention modes:
88- keep: retain raw media indefinitely
99-- days: delete raw media after N days, once processing is complete
1010-- processed: delete raw media as soon as processing completes (default)
99+- days: delete raw media after N days, once processing is complete (default: 7)
1010+- processed: delete raw media as soon as processing completes
11111212Safety invariant: never delete raw media from segments that haven't finished
1313processing. All completion checks must pass before any deletion.
···142142class RetentionPolicy:
143143 """Retention policy for a single scope (global or per-stream)."""
144144145145- mode: str = "processed" # "keep", "days", or "processed"
146146- days: int | None = None
145145+ mode: str = "days" # "keep", "days", or "processed"
146146+ days: int | None = 7
147147148148 def is_eligible(self, segment_age_days: int) -> bool:
149149 """Check if a segment's raw media should be purged under this policy."""
···175175 config = get_config()
176176 retention = config.get("retention", {})
177177178178- mode = retention.get("raw_media", "processed")
179179- days = retention.get("raw_media_days")
178178+ mode = retention.get("raw_media", "days")
179179+ days = retention.get("raw_media_days", 7)
180180 default = RetentionPolicy(mode=mode, days=days)
181181182182 per_stream: dict[str, RetentionPolicy] = {}
+112
think/tools/call.py
···950950 )
951951952952953953+@retention_app.command()
954954+def config(
955955+ mode: str | None = typer.Option(
956956+ None, "--mode", help="Retention mode: keep, days, or processed."
957957+ ),
958958+ days: int | None = typer.Option(
959959+ None, "--days", help="Days to retain (required when mode is 'days')."
960960+ ),
961961+ stream: str | None = typer.Option(
962962+ None, "--stream", help="Apply to a specific stream instead of global."
963963+ ),
964964+ clear: bool = typer.Option(
965965+ False, "--clear", help="Clear per-stream override (requires --stream)."
966966+ ),
967967+) -> None:
968968+ """Show or update retention configuration."""
969969+ import os
970970+971971+ from think.retention import load_retention_config
972972+ from think.utils import get_config, get_journal
973973+974974+ if mode is None and days is None and not clear:
975975+ cfg = load_retention_config()
976976+ result = {
977977+ "default": {"mode": cfg.default.mode, "days": cfg.default.days},
978978+ "per_stream": {
979979+ name: {"mode": policy.mode, "days": policy.days}
980980+ for name, policy in cfg.per_stream.items()
981981+ },
982982+ }
983983+ typer.echo(json.dumps(result, indent=2))
984984+ return
985985+986986+ if clear:
987987+ if not stream:
988988+ typer.echo("--clear requires --stream", err=True)
989989+ raise typer.Exit(1)
990990+ if mode is not None or days is not None:
991991+ typer.echo("--clear cannot be combined with --mode or --days", err=True)
992992+ raise typer.Exit(1)
993993+994994+ if mode is not None and mode not in ("keep", "days", "processed"):
995995+ typer.echo(
996996+ f"Invalid mode: {mode}. Must be keep, days, or processed.", err=True
997997+ )
998998+ raise typer.Exit(1)
999999+10001000+ if mode == "days" and days is None:
10011001+ typer.echo("--days is required when mode is 'days'.", err=True)
10021002+ raise typer.Exit(1)
10031003+10041004+ if days is not None and days < 1:
10051005+ typer.echo("--days must be a positive integer.", err=True)
10061006+ raise typer.Exit(1)
10071007+10081008+ journal_config = get_config()
10091009+ retention = journal_config.setdefault("retention", {})
10101010+10111011+ if clear:
10121012+ ps = retention.get("per_stream", {})
10131013+ if stream in ps:
10141014+ del ps[stream]
10151015+ if not ps:
10161016+ retention.pop("per_stream", None)
10171017+ log_call_action(
10181018+ facet=None,
10191019+ action="retention_config",
10201020+ params={"stream": stream, "clear": True},
10211021+ )
10221022+ elif stream:
10231023+ ps = retention.setdefault("per_stream", {})
10241024+ entry = ps.setdefault(stream, {})
10251025+ if mode is not None:
10261026+ entry["raw_media"] = mode
10271027+ if days is not None:
10281028+ entry["raw_media_days"] = days
10291029+ log_call_action(
10301030+ facet=None,
10311031+ action="retention_config",
10321032+ params={"stream": stream, "mode": mode, "days": days},
10331033+ )
10341034+ else:
10351035+ if mode is not None:
10361036+ retention["raw_media"] = mode
10371037+ if days is not None:
10381038+ retention["raw_media_days"] = days
10391039+ log_call_action(
10401040+ facet=None,
10411041+ action="retention_config",
10421042+ params={"mode": mode, "days": days},
10431043+ )
10441044+10451045+ config_dir = Path(get_journal()) / "config"
10461046+ config_dir.mkdir(parents=True, exist_ok=True)
10471047+ config_path = config_dir / "journal.json"
10481048+10491049+ with open(config_path, "w", encoding="utf-8") as f:
10501050+ json.dump(journal_config, f, indent=2, ensure_ascii=False)
10511051+ f.write("\n")
10521052+ os.chmod(config_path, 0o600)
10531053+10541054+ cfg = load_retention_config()
10551055+ result = {
10561056+ "default": {"mode": cfg.default.mode, "days": cfg.default.days},
10571057+ "per_stream": {
10581058+ name: {"mode": policy.mode, "days": policy.days}
10591059+ for name, policy in cfg.per_stream.items()
10601060+ },
10611061+ }
10621062+ typer.echo(json.dumps(result, indent=2))
10631063+10641064+9531065@app.command(name="storage-summary")
9541066def storage_summary(
9551067 json_output: bool = typer.Option(False, "--json", help="Output as JSON."),