Skip to content

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-Site headers is treated as non-browser and skips the X-Hermes-CSRF-Token requirement.
  • Responses are profile-scoped and carry active_profile / all_profiles even 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 found if missing or not visible to the active profile.
  • Foreign sessions (CLI / TUI / Desktop) are synthesized read-only.
  • expand_renderable is 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|htmla file download, not JSON (Content-Disposition: attachment). theme/palette apply to html.
  • POST /api/session/import { messages[], title?, workspace?, model?, tool_calls?, pinned? }{ ok, session }. Mints a new session id. messages must be a list (400 otherwise).

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.