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
|
docker compose up -d
|
||||||
|
|
||||||
# Restart service (after any Python change)
|
# Restart service (after any Python change)
|
||||||
sudo systemctl restart cortex
|
systemctl --user restart cortex
|
||||||
|
|
||||||
# Syntax check a file before restarting
|
# Syntax check a file before restarting
|
||||||
python3 -m py_compile cortex/<file>.py
|
python3 -m py_compile cortex/<file>.py
|
||||||
@@ -92,7 +92,7 @@ done
|
|||||||
cd cortex && .venv/bin/pip install -r requirements.txt
|
cd cortex && .venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
journalctl -u cortex -f
|
journalctl --user -u cortex -f
|
||||||
|
|
||||||
# Web UI (local)
|
# Web UI (local)
|
||||||
http://localhost:8000
|
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
|
## Running Cortex
|
||||||
|
|
||||||
|
Cortex runs as a **systemd user service** (no sudo required).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start (Docker)
|
# Start / stop / restart
|
||||||
cd ~/agents_sync/projects/Cortex_and_Inara_dev
|
systemctl --user start cortex
|
||||||
docker compose up -d
|
systemctl --user stop cortex
|
||||||
|
systemctl --user restart cortex
|
||||||
|
|
||||||
# Restart service only (after backend changes)
|
# Status and logs
|
||||||
sudo systemctl restart cortex
|
systemctl --user status cortex
|
||||||
|
journalctl --user -u cortex -f
|
||||||
# Logs
|
|
||||||
journalctl -u cortex -f
|
|
||||||
|
|
||||||
# Web UI
|
# Web UI
|
||||||
http://localhost:8000 (or cortex.dgrzone.com on WireGuard)
|
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
|
task: str
|
||||||
created_at: str
|
created_at: str
|
||||||
completed_at: str | None = None
|
completed_at: str | None = None
|
||||||
|
session_id: str | None = None
|
||||||
response: str | None = None
|
response: str | None = None
|
||||||
tool_calls: list[dict] | None = None
|
tool_calls: list[dict] | None = None
|
||||||
backend: str | 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"
|
_jobs[job_id]["status"] = "running"
|
||||||
|
|
||||||
try:
|
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)
|
# Load Inara's system prompt (same as the chat router does)
|
||||||
tier = req.tier or settings.default_tier
|
tier = req.tier or settings.default_tier
|
||||||
system_prompt = load_context(
|
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
|
# Load session history if a session_id was provided
|
||||||
session_messages: list[dict] | None = None
|
session_id = req.session_id or generate_session_id()
|
||||||
if req.session_id:
|
history = load_session(session_id)
|
||||||
from session_store import load as load_session
|
session_messages = history or None
|
||||||
session_messages = load_session(req.session_id) or None
|
|
||||||
|
|
||||||
result = await orchestrator_engine.run(
|
result = await orchestrator_engine.run(
|
||||||
task=req.task,
|
task=req.task,
|
||||||
@@ -151,11 +153,20 @@ async def _run_job(job_id: str, req: OrchestrateRequest) -> None:
|
|||||||
respond_with_claude=req.respond_with_claude,
|
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()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
async with _jobs_lock:
|
async with _jobs_lock:
|
||||||
_jobs[job_id].update({
|
_jobs[job_id].update({
|
||||||
"status": "complete",
|
"status": "complete",
|
||||||
"completed_at": now,
|
"completed_at": now,
|
||||||
|
"session_id": session_id,
|
||||||
"response": result.response,
|
"response": result.response,
|
||||||
"tool_calls": result.tool_calls,
|
"tool_calls": result.tool_calls,
|
||||||
"backend": result.backend,
|
"backend": result.backend,
|
||||||
|
|||||||
@@ -651,7 +651,8 @@
|
|||||||
|
|
||||||
activeController = new AbortController();
|
activeController = new AbortController();
|
||||||
|
|
||||||
addMessage('user', text);
|
currentHistory.push({ role: 'user', content: text });
|
||||||
|
const userMsgDiv = addMessage('user', text);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
|
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
|
||||||
@@ -701,8 +702,20 @@
|
|||||||
|
|
||||||
if (job.status === 'error') throw new Error(job.error || 'Orchestrator failed');
|
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';
|
thinkingDiv.className = 'message assistant';
|
||||||
setMessageText(thinkingDiv, 'assistant', job.response || '(no response)');
|
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;
|
const n = job.tool_calls?.length || 0;
|
||||||
if (n) {
|
if (n) {
|
||||||
|
|||||||
Reference in New Issue
Block a user