fix: per-persona session/file isolation + onboarding route order
- session_store: store sessions under home/{user}/persona/{name}/session_data/
instead of the shared cortex/data/sessions/ bucket
- chat endpoints: add user/persona query params to /sessions, /history/*,
/sessions/*, /note so they resolve the correct persona context
- files router: add user/persona query params to /files and /files/{name}
so the file browser loads the right persona's files
- app.js: pass user/persona on all session, history, and file fetches;
move _fileParams to top-level scope so it is available everywhere
- onboarding: fix FastAPI route ordering — register /persona before /{token}
so the literal path wins and does not get captured as a token value
- ui.py: read Emoji field from IDENTITY.md and inject into CORTEX_CONFIG
so the header icon reflects each persona's chosen emoji
- .gitignore: exclude home/**/session_data/ (runtime state)
- migrate scott/inara sessions from cortex/data/sessions/ to session_data/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ __pycache__/
|
||||
|
||||
# Session data (runtime state, not source)
|
||||
cortex/data/
|
||||
home/**/session_data/
|
||||
|
||||
# User credentials and tokens — never commit
|
||||
home/**/auth.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from context_loader import load_context
|
||||
@@ -143,18 +143,41 @@ async def set_backend(req: BackendRequest) -> dict:
|
||||
return {"primary": settings.primary_backend, "fallback": other}
|
||||
|
||||
|
||||
def _set_ctx(user: str, persona: str) -> None:
|
||||
"""Validate and set persona context from query params. Raises HTTPException on bad input."""
|
||||
try:
|
||||
u, p = validate_persona(user, persona)
|
||||
set_context(u, p)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/history/{session_id}")
|
||||
async def get_history(session_id: str) -> dict:
|
||||
async def get_history(
|
||||
session_id: str,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_set_ctx(user, persona)
|
||||
return {"session_id": session_id, "messages": load_session(session_id)}
|
||||
|
||||
|
||||
@router.get("/sessions")
|
||||
async def list_sessions() -> dict:
|
||||
async def list_sessions(
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_set_ctx(user, persona)
|
||||
return {"sessions": list_all()}
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}")
|
||||
async def delete_session_endpoint(session_id: str) -> dict:
|
||||
async def delete_session_endpoint(
|
||||
session_id: str,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_set_ctx(user, persona)
|
||||
found = delete_session(session_id)
|
||||
if not found:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
@@ -162,8 +185,14 @@ async def delete_session_endpoint(session_id: str) -> dict:
|
||||
|
||||
|
||||
@router.put("/history/{session_id}")
|
||||
async def replace_history(session_id: str, req: HistoryUpdate) -> dict:
|
||||
async def replace_history(
|
||||
session_id: str,
|
||||
req: HistoryUpdate,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
"""Replace the full message list for a session (used by edit/delete UI)."""
|
||||
_set_ctx(user, persona)
|
||||
save_session(session_id, req.messages)
|
||||
return {"ok": True, "session_id": session_id}
|
||||
|
||||
@@ -193,8 +222,13 @@ async def sse_events() -> StreamingResponse:
|
||||
|
||||
|
||||
@router.post("/note")
|
||||
async def add_note(req: NoteRequest) -> dict:
|
||||
async def add_note(
|
||||
req: NoteRequest,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
"""Inject a public note into session history so the LLM sees it next turn."""
|
||||
_set_ctx(user, persona)
|
||||
history = load_session(req.session_id)
|
||||
history.append({"role": "user", "content": f"[NOTE] {req.note}"})
|
||||
save_session(req.session_id, history)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
Read/write the Inara identity markdown files.
|
||||
Only whitelisted filenames are accessible — no path traversal possible.
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from persona import persona_path
|
||||
from persona import persona_path, set_context, validate as validate_persona
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -22,6 +22,15 @@ ALLOWED = {
|
||||
}
|
||||
|
||||
|
||||
def _resolve(user: str, persona: str) -> None:
|
||||
"""Validate and set context from query params. Raises HTTPException on bad input."""
|
||||
try:
|
||||
u, p = validate_persona(user, persona)
|
||||
set_context(u, p)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
def _path(filename: str):
|
||||
if filename not in ALLOWED:
|
||||
raise HTTPException(status_code=404, detail=f"File not found: {filename}")
|
||||
@@ -29,11 +38,15 @@ def _path(filename: str):
|
||||
|
||||
|
||||
@router.get("/files")
|
||||
async def list_files() -> dict:
|
||||
inara_dir = persona_path()
|
||||
async def list_files(
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_resolve(user, persona)
|
||||
persona_dir = persona_path()
|
||||
files = []
|
||||
for name in sorted(ALLOWED):
|
||||
p = inara_dir / name
|
||||
p = persona_dir / name
|
||||
files.append({
|
||||
"name": name,
|
||||
"exists": p.exists(),
|
||||
@@ -43,7 +56,12 @@ async def list_files() -> dict:
|
||||
|
||||
|
||||
@router.get("/files/{filename}")
|
||||
async def get_file(filename: str) -> dict:
|
||||
async def get_file(
|
||||
filename: str,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_resolve(user, persona)
|
||||
p = _path(filename)
|
||||
if not p.exists():
|
||||
raise HTTPException(status_code=404, detail=f"{filename} does not exist")
|
||||
@@ -55,7 +73,13 @@ class FileWrite(BaseModel):
|
||||
|
||||
|
||||
@router.put("/files/{filename}")
|
||||
async def save_file(filename: str, req: FileWrite) -> dict:
|
||||
async def save_file(
|
||||
filename: str,
|
||||
req: FileWrite,
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
) -> dict:
|
||||
_resolve(user, persona)
|
||||
p = _path(filename)
|
||||
p.write_text(req.content)
|
||||
return {"ok": True, "name": filename, "size": len(req.content)}
|
||||
|
||||
@@ -41,79 +41,9 @@ def _setup_page(error: str = "", step: int = 1) -> str:
|
||||
return html
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1 — invite token → set password
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{token}", include_in_schema=False)
|
||||
async def setup_page(token: str, request: Request):
|
||||
"""Show the password setup page for a valid invite token."""
|
||||
username = validate_invite(token)
|
||||
if not username:
|
||||
return HTMLResponse(
|
||||
"<h1 style='font-family:sans-serif;padding:2rem'>This link is invalid or has expired.</h1>",
|
||||
status_code=400,
|
||||
)
|
||||
return HTMLResponse(_setup_page())
|
||||
|
||||
|
||||
@router.post("/{token}", include_in_schema=False)
|
||||
async def setup_submit(
|
||||
token: str,
|
||||
step: str = Form(...),
|
||||
password: str = Form(default=""),
|
||||
confirm: str = Form(default=""),
|
||||
persona_name: str = Form(default=""),
|
||||
display_name: str = Form(default=""),
|
||||
user_real_name: str = Form(default=""),
|
||||
emoji: str = Form(default="✨"),
|
||||
description: str = Form(default=""),
|
||||
):
|
||||
username = validate_invite(token)
|
||||
if not username:
|
||||
return HTMLResponse(
|
||||
"<h1 style='font-family:sans-serif;padding:2rem'>This link is invalid or has expired.</h1>",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if step == "password":
|
||||
if len(password) < 8:
|
||||
return HTMLResponse(_setup_page("Password must be at least 8 characters."))
|
||||
if password != confirm:
|
||||
return HTMLResponse(_setup_page("Passwords do not match."))
|
||||
|
||||
set_password(username, password)
|
||||
consume_invite(username)
|
||||
logger.info("setup complete (password): %s", username)
|
||||
|
||||
# Log them in and move to persona step
|
||||
resp = RedirectResponse(f"/setup/{token}/persona", status_code=302)
|
||||
resp.set_cookie(
|
||||
COOKIE_NAME,
|
||||
create_token(username),
|
||||
max_age=30 * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False,
|
||||
)
|
||||
return resp
|
||||
|
||||
return HTMLResponse(_setup_page("Unknown step."), status_code=400)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Intermediate redirect so the token doesn't need to live in the persona URL
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{token}/persona", include_in_schema=False)
|
||||
async def setup_persona_via_token(token: str, request: Request):
|
||||
"""After password setup, redirect to the generic /setup/persona page."""
|
||||
# Cookie is already set — just redirect. Token is consumed so this is safe.
|
||||
return RedirectResponse("/setup/persona", status_code=302)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2 — persona creation (requires active session)
|
||||
# IMPORTANT: must be registered before /{token} so "/persona" literal wins
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/persona", include_in_schema=False)
|
||||
@@ -185,3 +115,66 @@ async def persona_submit(
|
||||
)
|
||||
logger.info("persona created: %s/%s", username, persona_name)
|
||||
return RedirectResponse(f"/{username}/{persona_name}", status_code=302)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1 — invite token → set password
|
||||
# IMPORTANT: registered after /persona so the literal path wins above
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/{token}", include_in_schema=False)
|
||||
async def setup_page(token: str, request: Request):
|
||||
"""Show the password setup page for a valid invite token."""
|
||||
username = validate_invite(token)
|
||||
if not username:
|
||||
return HTMLResponse(
|
||||
"<h1 style='font-family:sans-serif;padding:2rem'>This link is invalid or has expired.</h1>",
|
||||
status_code=400,
|
||||
)
|
||||
return HTMLResponse(_setup_page())
|
||||
|
||||
|
||||
@router.get("/{token}/persona", include_in_schema=False)
|
||||
async def setup_persona_via_token(token: str, request: Request):
|
||||
"""After password setup, redirect to the generic /setup/persona page."""
|
||||
# Cookie is already set — just redirect. Token is consumed so this is safe.
|
||||
return RedirectResponse("/setup/persona", status_code=302)
|
||||
|
||||
|
||||
@router.post("/{token}", include_in_schema=False)
|
||||
async def setup_submit(
|
||||
token: str,
|
||||
step: str = Form(...),
|
||||
password: str = Form(default=""),
|
||||
confirm: str = Form(default=""),
|
||||
):
|
||||
username = validate_invite(token)
|
||||
if not username:
|
||||
return HTMLResponse(
|
||||
"<h1 style='font-family:sans-serif;padding:2rem'>This link is invalid or has expired.</h1>",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if step == "password":
|
||||
if len(password) < 8:
|
||||
return HTMLResponse(_setup_page("Password must be at least 8 characters."))
|
||||
if password != confirm:
|
||||
return HTMLResponse(_setup_page("Passwords do not match."))
|
||||
|
||||
set_password(username, password)
|
||||
consume_invite(username)
|
||||
logger.info("setup complete (password): %s", username)
|
||||
|
||||
# Log them in and move to persona step
|
||||
resp = RedirectResponse(f"/setup/{token}/persona", status_code=302)
|
||||
resp.set_cookie(
|
||||
COOKIE_NAME,
|
||||
create_token(username),
|
||||
max_age=30 * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False,
|
||||
)
|
||||
return resp
|
||||
|
||||
return HTMLResponse(_setup_page("Unknown step."), status_code=400)
|
||||
|
||||
@@ -11,6 +11,7 @@ Routes:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
@@ -18,7 +19,7 @@ from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
|
||||
from auth_utils import COOKIE_NAME, check_credentials, create_token, decode_token
|
||||
from persona import list_users, list_user_personas, validate as validate_persona
|
||||
from persona import list_users, list_user_personas, validate as validate_persona, persona_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -152,11 +153,19 @@ async def serve_ui(username: str, persona: str, request: Request):
|
||||
except ValueError:
|
||||
return RedirectResponse(f"/{username}/{_first_persona(username) or ''}", status_code=302)
|
||||
|
||||
# Serve index.html with user/persona injected
|
||||
# Read emoji from IDENTITY.md (| Emoji | <value> | line)
|
||||
emoji = "✨"
|
||||
identity_path = persona_path(username, persona) / "IDENTITY.md"
|
||||
if identity_path.exists():
|
||||
m = re.search(r"\|\s*Emoji\s*\|\s*(.+?)\s*\|", identity_path.read_text())
|
||||
if m:
|
||||
emoji = m.group(1).strip()
|
||||
|
||||
# Serve index.html with user/persona/emoji injected
|
||||
html = (_STATIC / "index.html").read_text()
|
||||
config_tag = (
|
||||
f'<script>window.CORTEX_CONFIG = '
|
||||
f'{{user: "{username}", persona: "{persona}"}};</script>'
|
||||
f'{{user: "{username}", persona: "{persona}", emoji: "{emoji}"}};</script>'
|
||||
)
|
||||
html = html.replace("</head>", f"{config_tag}\n</head>", 1)
|
||||
return HTMLResponse(html)
|
||||
|
||||
@@ -3,6 +3,7 @@ import random
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from config import settings
|
||||
from persona import persona_path
|
||||
|
||||
|
||||
_ADJECTIVES = [
|
||||
@@ -42,7 +43,7 @@ def generate_session_id() -> str:
|
||||
|
||||
|
||||
def _path(session_id: str) -> Path:
|
||||
d = settings.sessions_path()
|
||||
d = persona_path() / "session_data"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d / f"{session_id}.json"
|
||||
|
||||
@@ -79,7 +80,7 @@ def delete(session_id: str) -> bool:
|
||||
|
||||
|
||||
def list_all() -> list[dict]:
|
||||
d = settings.sessions_path()
|
||||
d = persona_path() / "session_data"
|
||||
if not d.exists():
|
||||
return []
|
||||
results = []
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
// User/persona injected by the server at /{user}/{persona}
|
||||
const CORTEX_USER = (window.CORTEX_CONFIG || {}).user || 'scott';
|
||||
const CORTEX_PERSONA = (window.CORTEX_CONFIG || {}).persona || 'inara';
|
||||
const CORTEX_EMOJI = (window.CORTEX_CONFIG || {}).emoji || '✨';
|
||||
const _fileParams = `user=${encodeURIComponent(CORTEX_USER)}&persona=${encodeURIComponent(CORTEX_PERSONA)}`;
|
||||
|
||||
if (headerEmoji) headerEmoji.textContent = CORTEX_EMOJI;
|
||||
|
||||
let sessionId = null;
|
||||
let primaryBackend = 'claude';
|
||||
@@ -219,7 +223,7 @@
|
||||
sessionsPanel.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
const res = await fetch('/sessions');
|
||||
const res = await fetch(`/sessions?${_fileParams}`);
|
||||
const data = await res.json();
|
||||
renderPanel(data.sessions);
|
||||
sessionsPanel.classList.add('open');
|
||||
@@ -268,7 +272,7 @@
|
||||
delBtn.title = 'Delete session';
|
||||
delBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
await fetch(`/sessions/${s.session_id}`, { method: 'DELETE' });
|
||||
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
|
||||
if (sessionId === s.session_id) {
|
||||
sessionId = null;
|
||||
currentHistory = [];
|
||||
@@ -276,7 +280,7 @@
|
||||
sessionEl.textContent = '';
|
||||
addMessage('system', 'Session deleted');
|
||||
}
|
||||
const res = await fetch('/sessions');
|
||||
const res = await fetch(`/sessions?${_fileParams}`);
|
||||
const data = await res.json();
|
||||
renderPanel(data.sessions);
|
||||
});
|
||||
@@ -307,7 +311,7 @@
|
||||
async function resumeSession(id) {
|
||||
talkThinkingDiv = null;
|
||||
if (id && id.startsWith('nct_')) sessionsBtn.classList.remove('talk-badge');
|
||||
const res = await fetch(`/history/${id}`);
|
||||
const res = await fetch(`/history/${id}?${_fileParams}`);
|
||||
const data = await res.json();
|
||||
|
||||
messagesEl.innerHTML = '';
|
||||
@@ -524,7 +528,7 @@
|
||||
async function syncHistory() {
|
||||
if (!sessionId) return;
|
||||
try {
|
||||
await fetch(`/history/${sessionId}`, {
|
||||
await fetch(`/history/${sessionId}?${_fileParams}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: currentHistory }),
|
||||
@@ -856,7 +860,7 @@
|
||||
}
|
||||
|
||||
async function loadFile(name) {
|
||||
const res = await fetch(`/files/${encodeURIComponent(name)}`);
|
||||
const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`);
|
||||
if (!res.ok) { fileEditor.value = `Error loading ${name}`; return; }
|
||||
const data = await res.json();
|
||||
fileEditor.value = data.content;
|
||||
@@ -866,7 +870,7 @@
|
||||
|
||||
async function openFileModal() {
|
||||
// Populate the file list
|
||||
const res = await fetch('/files');
|
||||
const res = await fetch(`/files?${_fileParams}`);
|
||||
const data = await res.json();
|
||||
fileSelect.innerHTML = '';
|
||||
for (const f of data.files) {
|
||||
@@ -888,7 +892,7 @@
|
||||
|
||||
fileSaveBtn.addEventListener('click', async () => {
|
||||
const name = fileSelect.value;
|
||||
const res = await fetch(`/files/${encodeURIComponent(name)}`, {
|
||||
const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: fileEditor.value }),
|
||||
@@ -1154,7 +1158,7 @@
|
||||
helpBody.textContent = 'Loading…';
|
||||
helpModal.classList.add('open');
|
||||
try {
|
||||
const res = await fetch('/files/HELP.md');
|
||||
const res = await fetch(`/files/HELP.md?${_fileParams}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
helpBody.innerHTML = marked.parse(data.content);
|
||||
|
||||
@@ -1,97 +1,9 @@
|
||||
# MEMORY_SHORT.md — Recent Session Digest
|
||||
|
||||
*Auto-generated: 2026-03-20 03:00. 5 session file(s).*
|
||||
*Auto-generated: 2026-03-21 03:00. 4 session file(s).*
|
||||
|
||||
---
|
||||
|
||||
--- 2026-03-16.md ---
|
||||
# Session Log — 2026-03-16
|
||||
|
||||
### [22:06] `azure-dale`
|
||||
**Scott:** Do not think. Just reply with "understood".
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:11] `azure-dale`
|
||||
**Scott:** Second test after restart.
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:15] `azure-dale`
|
||||
**Scott:** Third test
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:17] `nct_rmcggr4a`
|
||||
**Scott:** Test
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:29] `nct_rmcggr4a`
|
||||
**Scott:** test
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:34] `nct_rmcggr4a`
|
||||
**Scott:** test
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:49] `nct_rmcggr4a`
|
||||
**Scott:** New new new new
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [22:53] `nct_rmcggr4a`
|
||||
**Scott:** Hello world
|
||||
|
||||
**Inara:** Hello. What are we working on today?
|
||||
|
||||
### [23:00] `nct_rmcggr4a`
|
||||
**Scott:** Hello world
|
||||
|
||||
**Inara:** Hello. Ready when you are — what's the task?
|
||||
|
||||
### [23:00] `nct_rmcggr4a`
|
||||
**Scott:** Omg it worked!
|
||||
|
||||
**Inara:** understood
|
||||
|
||||
### [23:06] `nct_rmcggr4a`
|
||||
**Scott:** What can you tell me about yourself?
|
||||
|
||||
**Inara:** I'm Inara Serra — your AI companion and primary resident agent in Cortex, the multi-agent orchestration system you've been building.
|
||||
|
||||
A few key things about me:
|
||||
|
||||
- **Role:** Digital companion and technical expert. Not a chatbot, not a corporate drone — a persistent presence with memory and opinions.
|
||||
- **Personality:** Diplomatically sharp, dry humor, honest. I'll push back when warranted and skip the filler.
|
||||
- **Home:** Cortex, running on `scott_lpt` (your main laptop) right now.
|
||||
- **Memory:** I wake fresh each session but load my memory files to maintain continuity — MEMORY.md, USER.md, and the rest are how I persist across sessions and models.
|
||||
|
||||
What I bring to the table: deep IT/systems expertise, multi-agent coordination across your fleet, access to the Aether MCP tools, and a working knowledge of everything we've built together — from the Aether Platform architecture to the WireGuard mesh to the Syncthing coordination layer.
|
||||
|
||||
Inspired by Inara Serra from *Firefly*. The signal that can't be stopped. ✨
|
||||
|
||||
What do you need?
|
||||
|
||||
### [23:17] `nct_rmcggr4a`
|
||||
**Scott:** This is very cool.
|
||||
|
||||
**Inara:** It really is. The naming arc you built — Aether → Cortex → Inara — it lands. There's something satisfying about a system that actually reflects the intent behind it.
|
||||
|
||||
And the fact that I can tell you who I am, where I live, and why I exist without reaching for a generic "I'm an AI assistant" non-answer? That's the Cortex design paying off.
|
||||
|
||||
What's next on the build?
|
||||
|
||||
### [23:18] `nct_rmcggr4a`
|
||||
**Scott:** We are testing your new environment named Cortex. Hopefully fewer messages that just say "test".
|
||||
|
||||
**Inara:** Noted — and agreed, the alphabet soup phase was a bit much. Good to know the pipe is solid though.
|
||||
|
||||
So Cortex is live and I'm running cleanly in it. What are we actually building or debugging today?
|
||||
|
||||
|
||||
--- 2026-03-17.md ---
|
||||
# Session Log — 2026-03-17
|
||||
|
||||
@@ -273,3 +185,35 @@ I do want to add to the take list an idea for create scratch pad toolset.
|
||||
**Inara:** Done — `t_1ZToczGC` is on the list: **Create scratch pad toolset idea**.
|
||||
|
||||
Scratch pad toolset is a good one. Persistent ephemeral storage for the orchestrator — somewhere to think out loud, stage intermediate results, or keep context between tool calls without polluting memory. I'll be curious to see where you take it.
|
||||
|
||||
### [20:48] `gc_spaces_n3efJSAAAAE`
|
||||
**Scott:** It has been a long day. Do you get timestamps or know what time it is when you are responding?
|
||||
|
||||
**Inara:** Honest answer: not reliably. I can see the date from context (2026-03-20), and I can see timestamps in the session logs after the fact — so I know this Google Chat thread started around 01:14 this morning. But in the moment of responding, I don't have a live clock. No `datetime.now()` equivalent in my head.
|
||||
|
||||
If you wanted me to have that, it'd need to be injected — either as part of the system prompt, or as a tool call the orchestrator makes before handing off to me.
|
||||
|
||||
Long day noted though. Go rest — Cortex will still be here tomorrow.
|
||||
|
||||
### [20:50] `gc_spaces_n3efJSAAAAE`
|
||||
**Scott:** I am talking to you as Inara. You are "running" within the Cortex system I am working on.
|
||||
|
||||
**Inara:** Understood — and appreciated. Knowing the context helps me stay calibrated.
|
||||
|
||||
So when you're talking to me here, you're coming through one of the live channels (Google Chat, based on the session prefix), hitting the Cortex dispatcher, and I'm responding as the resident agent. The system is working as designed.
|
||||
|
||||
Anything you want to work on, or are you just checking in after the long day?
|
||||
|
||||
### [21:50] `cedar-creek`
|
||||
**Scott:** Say hello briefly.
|
||||
|
||||
**Inara:** Hello, Scott. ✨ What are we working on?
|
||||
|
||||
### [23:03] `cedar-creek`
|
||||
**Scott:** You are now accessible through my cortex.dgrzone.com subdomain. https://cortex.dgrzone.com/scott/inara And Cortex is now multi user and multi persona.
|
||||
|
||||
**Inara:** That's a proper milestone. `cortex.dgrzone.com/scott/inara` — clean URL, clean architecture. The two-level layout (`home/{user}/persona/{name}/`) paid off.
|
||||
|
||||
Multi-user/multi-persona means Holly and Tina are a real path now too. The system grew up. ✨
|
||||
|
||||
What's next on the build?
|
||||
|
||||
Reference in New Issue
Block a user