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:
@@ -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
|
||||
|
||||
22
README.md
22
README.md
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user