feat: persist orchestrator sessions + user service + docs update

- Orchestrator now saves turns to session store so history survives page refresh
- UI session_id updated from job result; history controls attached to agent turns
- Cortex migrated from system service to systemd user service (no more sudo)
- Update README.md and CLAUDE.md with correct service commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-18 23:08:38 -04:00
parent 5b5586656f
commit aaac3e1353
4 changed files with 44 additions and 16 deletions

View File

@@ -78,7 +78,7 @@ Cortex_and_Inara_dev/
docker compose up -d
# Restart service (after any Python change)
sudo systemctl restart cortex
systemctl --user restart cortex
# Syntax check a file before restarting
python3 -m py_compile cortex/<file>.py
@@ -92,7 +92,7 @@ done
cd cortex && .venv/bin/pip install -r requirements.txt
# Logs
journalctl -u cortex -f
journalctl --user -u cortex -f
# Web UI (local)
http://localhost:8000

View File

@@ -23,22 +23,26 @@ Cortex is a self-hosted multi-agent orchestration layer. Inara is the primary co
## Running Cortex
Cortex runs as a **systemd user service** (no sudo required).
```bash
# Start (Docker)
cd ~/agents_sync/projects/Cortex_and_Inara_dev
docker compose up -d
# Start / stop / restart
systemctl --user start cortex
systemctl --user stop cortex
systemctl --user restart cortex
# Restart service only (after backend changes)
sudo systemctl restart cortex
# Logs
journalctl -u cortex -f
# Status and logs
systemctl --user status cortex
journalctl --user -u cortex -f
# Web UI
http://localhost:8000 (or cortex.dgrzone.com on WireGuard)
```
Config lives in `cortex/config.py` and a `.env` file at the project root (not tracked — see `env.default`).
The service starts automatically at boot via `loginctl enable-linger`.
Service file: `~/.config/systemd/user/cortex.service`
Config lives in `cortex/config.py` and a `.env` file at the project root (not tracked — see `.env.default`).
---

View File

@@ -59,6 +59,7 @@ class JobStatusResponse(BaseModel):
task: str
created_at: str
completed_at: str | None = None
session_id: str | None = None
response: str | None = None
tool_calls: list[dict] | None = None
backend: str | None = None
@@ -129,6 +130,8 @@ async def _run_job(job_id: str, req: OrchestrateRequest) -> None:
_jobs[job_id]["status"] = "running"
try:
from session_store import load as load_session, save as save_session, generate_session_id
# Load Inara's system prompt (same as the chat router does)
tier = req.tier or settings.default_tier
system_prompt = load_context(
@@ -139,10 +142,9 @@ async def _run_job(job_id: str, req: OrchestrateRequest) -> None:
)
# Load session history if a session_id was provided
session_messages: list[dict] | None = None
if req.session_id:
from session_store import load as load_session
session_messages = load_session(req.session_id) or None
session_id = req.session_id or generate_session_id()
history = load_session(session_id)
session_messages = history or None
result = await orchestrator_engine.run(
task=req.task,
@@ -151,11 +153,20 @@ async def _run_job(job_id: str, req: OrchestrateRequest) -> None:
respond_with_claude=req.respond_with_claude,
)
# Save the turn to the session store so it survives a page refresh
history.append({"role": "user", "content": req.task})
history.append({"role": "assistant", "content": result.response})
save_session(session_id, history)
from session_logger import log_turn
log_turn(session_id, req.task, result.response)
now = datetime.now(timezone.utc).isoformat()
async with _jobs_lock:
_jobs[job_id].update({
"status": "complete",
"completed_at": now,
"session_id": session_id,
"response": result.response,
"tool_calls": result.tool_calls,
"backend": result.backend,

View File

@@ -651,7 +651,8 @@
activeController = new AbortController();
addMessage('user', text);
currentHistory.push({ role: 'user', content: text });
const userMsgDiv = addMessage('user', text);
scrollToBottom();
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
@@ -701,8 +702,20 @@
if (job.status === 'error') throw new Error(job.error || 'Orchestrator failed');
// Update session so this turn is part of the resumable history
if (job.session_id) {
sessionId = job.session_id;
sessionEl.textContent = `session: ${sessionId}`;
}
const userHistIdx = currentHistory.length - 1; // pushed before fetch
attachHistoryControls(userMsgDiv, userHistIdx);
thinkingDiv.className = 'message assistant';
setMessageText(thinkingDiv, 'assistant', job.response || '(no response)');
const assistHistIdx = currentHistory.length;
currentHistory.push({ role: 'assistant', content: job.response || '' });
attachHistoryControls(thinkingDiv, assistHistIdx);
const n = job.tool_calls?.length || 0;
if (n) {