feat: multi-user/multi-persona support with two-level home directory layout
Restructures persona storage from a flat personas/{name}/ layout to
home/{username}/persona/{name}/, mirroring Linux home directories.
Changes:
- persona.py: two ContextVars (user + persona), Linux-style name validation,
set_context(), get_user(), get_persona(), validate(), list_users(),
list_user_personas(); persona_path() takes (username, name)
- config.py: replaces personas_dir with home_dir + home_root()
- git mv personas/inara → home/scott/persona/inara (history preserved)
- home/holly/persona/tina/: Holly's persona stub added
- cron_runner.py: all storage functions take (username, persona) params
- tools/cron.py: stamps user + persona on jobs; APScheduler IDs are
{user}:{persona}:{job_id} to prevent collisions across users
- memory_distiller.py: distill_short/mid/long take (username, persona);
added missing Path + settings imports
- scheduler.py: _load_user_crons() iterates home/*/persona/* (two-level)
- routers/chat.py, orchestrator.py: user field added; set_context() called
- tests/conftest.py: home_root fixture with two-level structure;
patches home_dir instead of personas_dir
- tests/test_persona.py: fully rewritten for two-level API
- tests/test_api_files.py: updated fixture name and path
- .env.default: documents HOME_DIR setting; scrubs stale API key
- CLAUDE.md, README.md: directory maps updated for new layout
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from persona import persona_path, get_persona
|
||||
from persona import persona_path, get_user, get_persona
|
||||
from cron_runner import load_crons, save_crons, parse_schedule
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def _short_id() -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cron_list() -> str:
|
||||
crons = load_crons()
|
||||
crons = load_crons(get_user(), get_persona())
|
||||
if not crons:
|
||||
return "No crons scheduled."
|
||||
|
||||
@@ -60,10 +60,12 @@ def _cron_add(label: str, schedule: str, job_type: str, payload: str) -> str:
|
||||
if job_type not in ("remind", "note"):
|
||||
return "Bad type: must be 'remind' or 'note'."
|
||||
|
||||
crons = load_crons()
|
||||
current_user = get_user()
|
||||
current_persona = get_persona()
|
||||
crons = load_crons(current_user, current_persona)
|
||||
job = {
|
||||
"id": _short_id(),
|
||||
"user": current_user,
|
||||
"persona": current_persona,
|
||||
"label": label,
|
||||
"schedule": schedule,
|
||||
@@ -74,35 +76,37 @@ def _cron_add(label: str, schedule: str, job_type: str, payload: str) -> str:
|
||||
"last_run": None,
|
||||
}
|
||||
crons.append(job)
|
||||
save_crons(crons, current_persona)
|
||||
save_crons(crons, current_user, current_persona)
|
||||
|
||||
# Register with the live scheduler
|
||||
_scheduler_add(job, sched_kwargs)
|
||||
|
||||
return f"Created and scheduled: {job['id']} {schedule} [{job_type}] {label} (persona: {current_persona})"
|
||||
return f"Created and scheduled: {job['id']} {schedule} [{job_type}] {label} (user: {current_user}, persona: {current_persona})"
|
||||
|
||||
|
||||
def _cron_remove(cron_id: str) -> str:
|
||||
user = get_user()
|
||||
persona = get_persona()
|
||||
crons = load_crons(persona)
|
||||
crons = load_crons(user, persona)
|
||||
before = len(crons)
|
||||
crons = [c for c in crons if c["id"] != cron_id]
|
||||
if len(crons) == before:
|
||||
return f"Not found: {cron_id}"
|
||||
save_crons(crons, persona)
|
||||
_scheduler_remove(f"{persona}:{cron_id}")
|
||||
save_crons(crons, user, persona)
|
||||
_scheduler_remove(f"{user}:{persona}:{cron_id}")
|
||||
return f"Removed: {cron_id}"
|
||||
|
||||
|
||||
def _cron_toggle(cron_id: str) -> str:
|
||||
user = get_user()
|
||||
persona = get_persona()
|
||||
crons = load_crons(persona)
|
||||
crons = load_crons(user, persona)
|
||||
for c in crons:
|
||||
if c["id"] == cron_id:
|
||||
c["enabled"] = not c.get("enabled", True)
|
||||
save_crons(crons, persona)
|
||||
save_crons(crons, user, persona)
|
||||
action = "resumed" if c["enabled"] else "paused"
|
||||
sched_id = f"{persona}:{cron_id}"
|
||||
sched_id = f"{user}:{persona}:{cron_id}"
|
||||
_scheduler_resume(sched_id) if c["enabled"] else _scheduler_pause(sched_id)
|
||||
return f"{action.capitalize()}: {cron_id} {c['label']}"
|
||||
return f"Not found: {cron_id}"
|
||||
@@ -125,7 +129,7 @@ def _scheduler_add(job: dict, sched_kwargs: dict) -> None:
|
||||
from cron_runner import run_job
|
||||
s = sched_module.get_scheduler()
|
||||
if s and s.running:
|
||||
sched_id = f"{job.get('persona', 'inara')}:{job['id']}"
|
||||
sched_id = f"{job.get('user', 'scott')}:{job.get('persona', 'inara')}:{job['id']}"
|
||||
s.add_job(
|
||||
lambda j=job: asyncio.ensure_future(run_job(j)),
|
||||
"cron",
|
||||
|
||||
Reference in New Issue
Block a user