Sessions & projects¶
The conversation sidebar and its full lifecycle. A session is one conversation; a project is a folder that groups sessions. Every field and status code below is verified against the server (api/routes.py, api/session_ops.py) and the reference client.
Conventions for this category
- Auth: all endpoints require a valid session when a password is set. See Authentication.
- Wire casing: request/response JSON is
snake_case. - Native clients are CSRF-exempt — a request without
Origin/Referer/Sec-Fetch-Siteheaders is treated as non-browser and skips theX-Hermes-CSRF-Tokenrequirement. - Responses are profile-scoped and carry
active_profile/all_profileseven where a client ignores them.
Sessions¶
GET /api/sessions — list (the sidebar)¶
Optional query params (all off by default): all_profiles, include_archived, exclude_hidden (flags); archived_limit (≤2000), archived_offset (≤200000); sidebar_source (webui | cli).
Response
{
"sessions": [ /* SessionRow[] */ ],
"sidebar_reference_sessions": [],
"cli_count": 0, "archived_count": 0,
"active_profile": "default", "all_profiles": false,
"server_time": 1712000000.0, "server_tz": "UTC"
}
SessionRow (key fields): session_id, title, display_title, workspace, model, model_provider, message_count, user_message_count, created_at, updated_at, last_message_at (floats), pinned, archived, project_id, profile, input_tokens, output_tokens, estimated_cost, cache_hit_percent, context_length, window_usage_percent, is_cli_session, is_streaming, active_stream_id, has_pending_user_message, worktree_path, worktree_branch, parent_session_id, relationship_type, read_only, attention. (Message bodies are intentionally excluded — fetch them via /api/session.)
GET /api/sessions/search¶
Params: q (required; empty → all sessions unranked), content (1/0, default 1 — also search message bodies), depth (default 5; 0 = full transcript, else last-N messages), all_profiles (flag).
Response — { "sessions": [...], "query": "...", "count": N, "active_profile": "..." }. Matched rows add match_type (title | content) and, for content hits, a redacted match_preview.
GET /api/session — one conversation + messages¶
Params: session_id (required, else 400), messages (1/0, default 1; 0 = metadata only — fast switch), msg_limit (tail window of last N visible rows), msg_before (0-based index for scroll-back paging), resolve_model (1/0).
Response — { "session": SessionDetail }. SessionDetail = a full SessionRow plus messages[], tool_calls[], pending_user_message, pending_attachments[], context_length, threshold_tokens, _messages_truncated, _messages_offset, compression_anchor_visible_idx, compression_anchor_summary.
404 Session not foundif missing or not visible to the active profile.- Foreign sessions (CLI / TUI / Desktop) are synthesized read-only.
expand_renderableis a legacy no-op — safe to omit.
GET /api/session/status¶
Param: session_id (required). Response — { session_id, title, model, profile, workspace, message_count, agent_running, active_stream_id, input_tokens, output_tokens, total_tokens, estimated_cost }. active_stream_id is present only while a stream is live in-process — a client uses this to decide whether to re-attach after backgrounding.
Lifecycle mutations (all POST)¶
| Endpoint | Body | Response | Notes |
|---|---|---|---|
/api/session/new |
{ workspace?, model?, model_provider?, profile?, worktree?, project_id? } |
{ session: SessionDetail } |
workspace validated → 400 on bad path |
/api/session/rename |
{ session_id, title } |
{ session: SessionRow } |
title trimmed, ≤80 chars; empty → "Untitled" |
/api/session/delete |
{ session_id } |
{ ok: true } |
purges session, index, attachments, journals |
/api/session/pin |
{ session_id, pinned=true } |
{ ok, session } |
enforces pin quota (default 3) → 400 if exceeded |
/api/session/archive |
{ session_id, archived=true } |
{ ok, session } |
does not bump updated_at |
/api/session/branch |
{ session_id, keep_count?, title? } |
{ session_id, title, parent_session_id } |
mints a new session id; fork of history |
/api/session/compress |
{ session_id, focus_topic? } |
{ ok, session, summary, focus_topic } |
needs ≥4 messages (400); 409 if streaming / mutated mid-run |
/api/session/undo |
{ session_id } |
{ ok, removed_count, removed_preview } |
⚠️ no-op returns { error } at HTTP 200 |
/api/session/retry |
{ session_id } |
{ ok, last_user_text, removed_count } |
⚠️ no-op at HTTP 200; client re-sends last_user_text |
/api/session/truncate |
{ session_id, keep_count } |
{ session: SessionDetail } |
trims to N messages |
/api/session/update |
{ session_id, workspace?, model?, model_provider? } |
{ session: SessionDetail } |
model change resets token counters + evicts cached agent |
/api/session/move |
{ session_id, project_id\|null } |
{ ok, session } |
⚠️ 503 if the session is busy streaming (5s lock) |
GET/POST /api/session/yolo — auto-approve toggle¶
- GET
?session_id=…→{ yolo_enabled }. - POST
{ session_id, enabled=true }→{ ok, yolo_enabled }. Enabling also resolves any pending approvals. In-memory flag — resets on server restart.
Export / import¶
GET /api/session/export?session_id=…&format=json|html→ a file download, not JSON (Content-Disposition: attachment).theme/paletteapply tohtml.POST /api/session/import{ messages[], title?, workspace?, model?, tool_calls?, pinned? }→{ ok, session }. Mints a new session id.messagesmust be a list (400otherwise).
Projects¶
GET /api/projects¶
Param: all_profiles (flag). Response — { "projects": [Project], "active_profile": "…" }. Project = { project_id, name, color|null, profile, created_at }.
Project mutations (all POST)¶
| Endpoint | Body | Response | Notes |
|---|---|---|---|
/api/projects/create |
{ name, color? } |
{ ok, project } |
name ≤128; color must match ^#[0-9a-fA-F]{3,8}$ → 400 otherwise |
/api/projects/rename |
{ project_id, name, color? } |
{ ok, project } |
404 if not owned by active profile |
/api/projects/delete |
{ project_id } |
{ ok } |
unassigns project_id from all member sessions |
Related: to open a conversation live, see Chat & streaming; to manage its files/git, see Workspace, files & git.