fix: multi-user distillation + datetime in context + session log labels

Distillation was silently operating on scott/inara for all users due to
ContextVar defaults. All three distill endpoints now require ?user=&persona=
query params and validate them via persona.validate(). Memory distiller
signatures changed from Optional to required positional args — no more
global settings fallback. Scheduler now iterates all users/personas instead
of hardcoding the primary user.

- context_loader: inject current date/time as first system prompt section
- session_logger: use get_user()/get_persona() from context instead of
  settings globals so Holly/Brian sessions show correct speaker labels
- memory_distiller: system prompts now reference u.title()/p.title()
  instead of settings.user_name/settings.agent_name
- distill router: Query(...) enforces params; _resolve() validates persona
- scheduler: _all_personas() helper iterates every user/persona for distill
- app.js: runDistill() now appends ?user=&persona= via _fileParams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-05 18:44:51 -04:00
parent 8d4aa4094c
commit 0ffcd57c95
6 changed files with 103 additions and 50 deletions

View File

@@ -19,41 +19,54 @@ logger = logging.getLogger(__name__)
_scheduler: AsyncIOScheduler | None = None
def _all_personas() -> list[tuple[str, str]]:
"""Return [(username, persona_name)] for every persona on this instance."""
from persona import list_users, list_user_personas
pairs = []
for u in list_users():
for p in list_user_personas(u):
pairs.append((u, p))
return pairs
async def _run_short() -> None:
from memory_distiller import distill_short
try:
result = distill_short()
logger.info("auto distill short: %d files, %d chars", result["files_included"], result["chars_written"])
except Exception as e:
logger.error("auto distill short failed: %s", e)
for u, p in _all_personas():
try:
result = distill_short(u, p)
logger.info("auto distill short [%s/%s]: %d files, %d chars", u, p, result["files_included"], result["chars_written"])
except Exception as e:
logger.error("auto distill short [%s/%s] failed: %s", u, p, e)
async def _run_mid() -> None:
from memory_distiller import distill_mid
from notification import notify
try:
result = await distill_mid()
if "error" in result:
logger.warning("auto distill mid skipped: %s", result["error"])
else:
logger.info("auto distill mid: %d chars via %s", result["chars_written"], result["backend"])
await notify(result["username"], f"📝 Weekly memory digest complete ({result['chars_written']} chars via {result['backend']}).")
except Exception as e:
logger.error("auto distill mid failed: %s", e)
for u, p in _all_personas():
try:
result = await distill_mid(u, p)
if "error" in result:
logger.warning("auto distill mid [%s/%s] skipped: %s", u, p, result["error"])
else:
logger.info("auto distill mid [%s/%s]: %d chars via %s", u, p, result["chars_written"], result["backend"])
await notify(u, f"📝 Weekly memory digest complete ({result['chars_written']} chars via {result['backend']}).")
except Exception as e:
logger.error("auto distill mid [%s/%s] failed: %s", u, p, e)
async def _run_long() -> None:
from memory_distiller import distill_long
from notification import notify
try:
result = await distill_long()
if "error" in result:
logger.warning("auto distill long skipped: %s", result["error"])
else:
logger.info("auto distill long: %d chars via %s", result["chars_written"], result["backend"])
await notify(result["username"], f"🧠 Monthly long-term memory integration complete ({result['chars_written']} chars via {result['backend']}). Worth a quick review.")
except Exception as e:
logger.error("auto distill long failed: %s", e)
for u, p in _all_personas():
try:
result = await distill_long(u, p)
if "error" in result:
logger.warning("auto distill long [%s/%s] skipped: %s", u, p, result["error"])
else:
logger.info("auto distill long [%s/%s]: %d chars via %s", u, p, result["chars_written"], result["backend"])
await notify(u, f"🧠 Monthly long-term memory integration complete ({result['chars_written']} chars via {result['backend']}). Worth a quick review.")
except Exception as e:
logger.error("auto distill long [%s/%s] failed: %s", u, p, e)
def get_scheduler() -> AsyncIOScheduler | None: