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

@@ -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) {