- Add cortex/persona.py: ContextVar-based per-request routing with path traversal protection and persona validation - Migrate inara/ → personas/inara/ (git history preserved via git mv) - config.py: add personas_root(), inara_path() delegates to personas/inara - All 14 settings.inara_path() call sites replaced with persona_path() - ChatRequest + OrchestrateRequest: add persona field (default: "inara") with validation at request entry before any processing - memory_distiller: add optional persona param for future per-persona distill - cron_runner/tools/cron: stamp persona on jobs, prefix APScheduler IDs (persona:job_id) to prevent collisions across personas - scheduler: _load_user_crons() iterates all personas at startup Adding a new persona: create personas/<name>/ with IDENTITY.md + SOUL.md. Auth: handled at nginx level (inject X-Cortex-Persona header per subdomain). Future: persona maps to Aether account_id_random for full integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
2.3 KiB
Python
80 lines
2.3 KiB
Python
"""
|
|
Scratchpad tools for Inara.
|
|
|
|
A lightweight, persistent notepad stored at inara/SCRATCH.md.
|
|
Nothing here is ever distilled or archived — it is intentionally transient.
|
|
Good for: working notes mid-task, half-formed ideas, things too long for
|
|
a chat response but not worth saving to memory or a journal entry.
|
|
|
|
Operations:
|
|
scratch_read — return the full contents (or a message if empty)
|
|
scratch_write — replace the entire scratchpad
|
|
scratch_append — add a new timestamped section at the bottom
|
|
scratch_clear — erase everything
|
|
"""
|
|
|
|
import asyncio
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from persona import persona_path
|
|
|
|
|
|
def _scratch_path() -> Path:
|
|
return persona_path() / "SCRATCH.md"
|
|
|
|
|
|
def _now_label() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sync implementations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _scratch_read() -> str:
|
|
p = _scratch_path()
|
|
if not p.exists() or not p.read_text().strip():
|
|
return "Scratchpad is empty."
|
|
return p.read_text()
|
|
|
|
|
|
def _scratch_write(content: str) -> str:
|
|
_scratch_path().write_text(content.rstrip() + "\n")
|
|
return "Scratchpad updated."
|
|
|
|
|
|
def _scratch_append(content: str, heading: str | None = None) -> str:
|
|
p = _scratch_path()
|
|
existing = p.read_text() if p.exists() else ""
|
|
label = heading or _now_label()
|
|
section = f"\n## {label}\n\n{content.strip()}\n"
|
|
p.write_text(existing.rstrip() + "\n" + section)
|
|
return f"Appended section: {label}"
|
|
|
|
|
|
def _scratch_clear() -> str:
|
|
p = _scratch_path()
|
|
p.write_text("")
|
|
return "Scratchpad cleared."
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async wrappers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def scratch_read() -> str:
|
|
return await asyncio.to_thread(_scratch_read)
|
|
|
|
|
|
async def scratch_write(content: str) -> str:
|
|
return await asyncio.to_thread(_scratch_write, content)
|
|
|
|
|
|
async def scratch_append(content: str, heading: str | None = None) -> str:
|
|
return await asyncio.to_thread(_scratch_append, content, heading)
|
|
|
|
|
|
async def scratch_clear() -> str:
|
|
return await asyncio.to_thread(_scratch_clear)
|