personal memory agent

fix(speakers): serve_audio 404s in prod — resolve media under chronicle day dir master

serve_audio built paths under the journal ROOT (state.journal_root/<day>/...) instead of the chronicle day dir (journal/chronicle/<day>/...), so every request 404'd in production. Its correct sibling serve_file (transcripts) had a divergent, hand-rolled containment idiom; that divergence is what let the bug hide. Extract one day-scoped containment helper safe_day_path(day, rel_path) in solstone/convey/utils.py, sibling to safe_journal_path. It contains a request rel_path under day_path(day), symlink-aware via contained_path, returning the standard INVALID_PATH envelope at HTTP 403 on any escape. Route both serve_audio and serve_file through it, fixing the base bug and removing the duplicated containment logic. Tests rework serve_audio to exercise the real journal root with chronicle-only media and no flat-day symlink, add safe_day_path unit tests including a date-like first-segment containment guard, and strengthen the serve_file traversal assertion to 403 + invalid_path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


+159 -97
6 changed files