Skip to content

Latest commit

 

History

History
262 lines (219 loc) · 11.8 KB

File metadata and controls

262 lines (219 loc) · 11.8 KB

Shelley HTTP/SSE API

This document describes the API contract between a Shelley server and its clients (the web UI, the iOS app, the CLI in client/, and tests). All routes are mounted under /api/ unless noted; the version endpoint is at /version.

Capabilities

GET /version

Returns build info plus capabilities (string list). Capabilities advertise optional, additive features that clients can opt into when present; a client that doesn't recognize a capability just doesn't use it, and an older server that doesn't ship the field is equivalent to advertising none.

Capabilities advertise optional behavior. Current capabilities include thinking-levels, drafts, and queued-transcriptions; clients that do not recognize one ignore it.

Stream architecture

The server derives the conversation list from the database and publishes RFC-6902 JSON Patch diffs on a single unified SSE stream:

  • GET /api/stream2 — unified SSE: per-conversation messages and conversation-list patches.
  • GET /api/conversations/snapshot — seed the patch stream with the current list and its content hash.
  • Previews are embedded in each conversation list row (preview, preview_updated_at).
  • GET /api/conversation/<id>/stream survives for non-web clients (iOS, CLI, tests) but new clients should use /api/stream2.

The patch stream is driven exclusively by Pool.OnCommit: every successful write transaction triggers a recompute, and a recomputeMu serializes recomputes so concurrent commits can't publish events out of order. Each event carries old_hash/new_hash, letting clients reconcile their state on every patch.

Endpoints

Versioning

  • GET /version{tag, commit, commit_time, capabilities: [...]}.
  • GET /version-check{has_update, current_tag, latest_tag, ...}.
  • GET /version-changelog — markdown changelog.

Conversation list

Unless noted, results exclude archived conversations.

  • GET /api/conversations?limit=&offset= — top-level (non-subagent) unarchived conversations as plain rows. Used by iOS and the CLI.
  • GET /api/conversations/snapshot — the current unarchived list including subagents, plus per-row state (working, git info, subagent count, preview) and the patch-stream hash. Used by the web UI on load. Response:
    {
      "conversations": [ConversationWithState, ...],
      "hash": "<sha256 hex>"
    }
  • GET /api/conversations/archived — archived list.
  • POST /api/conversations/new — create a conversation and post the first user message.
  • POST /api/conversations/distill-new-generation — compact the current conversation into the next generation of the same conversation. The optional method field (default or compact) is accepted for compatibility but is ignored: compaction is always used.

ConversationWithState row shape:

field meaning
conversation_id, slug, created_at, updated_at, cwd, archived, parent_conversation_id, model, conversation_options, current_generation, agent_working, user_initiated DB columns
working mirror of agent_working, kept for the patch-stream contract
git_repo_root, git_worktree_root, git_commit, git_subject optional, from a cached HEAD lookup keyed by cwd
subagent_count number of subagent conversations whose parent_conversation_id matches this row
preview, preview_updated_at trailing text of the most recent agent message and its timestamp (RFC 3339); empty if no agent reply yet, or if this conversation is outside the 500-most-recent window the server tracks for previews

Single conversation

  • GET /api/conversation/<id> — full message history (compressed).
  • GET /api/conversation/<id>/streamlegacy SSE: messages, state, no list patches. Used by iOS, CLI, and Go tests; new clients should use /api/stream2. Query params:
    • ?last_sequence_id=<n> — resume from sequence_id > n.
    • ?tail=<n> — first frame contains only the last n messages. A {"snapshot_complete": true} frame follows the initial replay and precedes live updates.
  • POST /api/conversation/<id>/chat — send a user message. A message of /transcription <absolute uploaded-media path> creates a durable specialized queued message and returns 202 {"status":"queued"}. The path must name a regular file inside /tmp/shelley-uploads. A hidden low-reasoning child performs the transcription independently of the HTTP request. While it is working or failed, the item reserves its FIFO position; when ready, the ordinary queue drainer creates the immutable parent user turn. Optional context follows the path on subsequent lines of the command and is prepended to the transcript, preserving text and ready attachments that were present before recording. Video results include the original recording and generated contact sheet paths in that turn.
  • POST /api/conversation/<id>/send-queued?queued_id=<id> — interrupt the running turn and immediately drain the FIFO head. The supplied id must still name that head item.
  • POST /api/conversation/<id>/cancel-queued?queued_id=<id> — cancel and remove one queued item. Omitting queued_id cancels the entire queue. Cancelling a working transcription also stops its hidden child.
  • POST /api/conversation/<id>/retry-queued?queued_id=<id> — retry a failed queued transcription in place.
  • POST /api/conversation/<id>/cancel — interrupt the running loop.
  • POST /api/conversation/<id>/archive / unarchive.
  • POST /api/conversation/<id>/hooks — register an end-of-turn webhook.
  • GET /api/conversation-by-slug/<slug> — lookup by slug.

Unified stream

GET /api/stream2?conversation=<id>&conversation_list_hash=<h>&last_sequence_id=<n>

SSE stream. A single connection delivers per-conversation events (messages, tool progress, stream deltas, conversation/state updates) for all active conversations on the server, plus the conversation-list patch stream. Every per-conversation event carries a top-level conversation_id field for client-side routing.

All query params are optional:

  • conversation — if set, the first frames replay that conversation's message history before live updates begin. It governs backfill only: live events for every conversation flow regardless.
  • conversation_list_hash — the hash from the most recent snapshot or patch event the client successfully applied. The server uses it to decide whether to replay history or send a fresh reset event.
  • last_sequence_id, tail — refine the conversation backfill, with the same semantics as on /api/conversation/<id>/stream. A snapshot_complete frame separates the initial replay (and an empty replay on connections without conversation) from live updates.

Event payload (data: <json>):

interface StreamResponse {
  // Routing key for per-conversation events. Always set on messages,
  // conversation, conversation_state, context_window_size, tool_progress,
  // and stream_delta. Empty for connection-scoped frames
  // (conversation_list_patch, heartbeat, snapshot_complete) and for
  // global events that already carry their own conversation reference
  // (notification_event).
  conversation_id?: string;

  // Per-conversation event payload. With a single stream serving every
  // active conversation, clients dispatch based on conversation_id.
  messages?: APIMessage[];
  conversation?: Conversation;
  conversation_state?: { conversation_id, working, model };
  context_window_size?: number;
  tool_progress?: ToolProgress;
  stream_delta?: StreamDelta;
  notification_event?: NotificationEvent;

  // Conversation-list patch stream:
  conversation_list_patch?: {
    old_hash: string | null,        // null on a reset event
    new_hash: string,
    patch: RFC6902Op[],             // ops with paths like "/0", "/0/working", etc.
    at: string,                     // RFC 3339
    reset?: true,                   // true for the seed event
  };

  heartbeat?: true;                 // sent every 30s if nothing else to say
  snapshot_complete?: true;         // once, after the initial replay
}

The conversation_list_patch operates on a document that is exactly the conversations array returned by /api/conversations/snapshot. Clients should:

  1. GET /api/conversations/snapshot once to obtain (state, hash).
  2. Open /api/stream2?conversation_list_hash=<hash>.
  3. For each conversation_list_patch event:
    • If event.old_hash == null or event.reset, replace local state with event.patch[0].value.
    • Otherwise, require event.old_hash == currentHash; apply event.patch via RFC 6902; assert hashList(state) == new_hash.
    • On any mismatch, drop local state and resume with the snapshot.

Reconnect semantics: the server keeps the last 100 patch events in memory. If the client's conversation_list_hash matches one of those boundaries, the server replays the missed patches; otherwise it sends a fresh reset event.

Git

  • GET /api/git/repos — repo discovery.
  • GET /api/git/diffs?cwd= — staged/unstaged file lists.
  • GET /api/git/diffs/<commit>?cwd= — committed file lists.
  • GET /api/git/file-diff/<path>?cwd=&base=&head= — unified diff.
  • GET /api/git/graph?cwd= — commit graph.
  • GET /api/git/commit-detail?cwd=&sha= — single commit.
  • GET /api/git/commit-messages?cwd= — recent commit messages.
  • POST /api/git/amend-message — amend HEAD message.
  • POST /api/git/create-worktreegit worktree add.

Files & directories

  • GET /api/list-directory?path= — directory listing.

  • POST /api/create-directorymkdir -p.

  • POST /api/write-file — write a file.

  • POST /api/upload — binary upload (multipart).

  • POST /api/upload/raw?filename= — binary upload with the file content as the request body (no multipart framing). Newer clients prefer this to avoid building a multipart body on device. Older servers return 404/405; clients should fall back to the multipart endpoint.

  • GET /api/upload/raw — empty 200 OK if the server supports the raw upload endpoint; clients use this as a capability probe (older servers return 404/405).

  • GET /api/read?path= — read a file (images served as image/*).

  • POST /api/validate-cwd — check whether a path is a valid working directory.

  • GET /api/user-agents-md / POST — read/write the per-user AGENTS.md.

Models, tools, notifications

  • GET /api/models — available models.
  • GET /api/tools — registered tool definitions.
  • GET/POST/PUT/DELETE /api/custom-models[/<id>] — custom model CRUD.
  • POST /api/custom-models-test — test a custom model config.
  • GET/POST/PUT/DELETE /api/notification-channels[/<id>], GET /api/notification-channel-types — notification CRUD.

Shell

  • WS /api/exec-ws?cwd= — websocket for an interactive shell session. cmd= starts a new persistent session; term_id= reattaches to an existing one and never reruns the command. conversation_id= records which conversation owns a newly spawned terminal.
  • GET /api/terminals — all persistent terminals, unfiltered. conversation_id is the owning conversation, or null for a terminal shown in every conversation.
  • PUT /api/terminals/<id>/scope — move a terminal between conversations. Body {"conversation_id": "<id>"} confines it to that conversation; {"conversation_id": null} shows it in all of them. null is the only accepted spelling of global: an absent field or an empty string is a 400. Returns the updated terminal.
  • DELETE /api/terminals/<id>, POST /api/terminals/<id>/kill — terminate a terminal and drop its record.

Debug

  • GET /debug/conversations — HTML dump of the conversation list.
  • GET /debug/conversation-stream — HTML viewer over the patch stream.
  • GET /debug/conversation-stream/history — JSON dump of the last 100 patch events.