How It Works
The slop behind the slop
This page explains Sesame's internal architecture and how sessions are executed.
Overview
Sesame is a monorepo application with a Hono backend API and TanStack Router frontend, orchestrating AI coding agents in sandboxed environments.
Monorepo Structure
Sesame uses Turborepo for monorepo management with the following structure:
Session Execution Flow
Step-by-Step
- User creates a session with a prompt and selects a repository
- API validates the request and creates a session record
- SandboxProvider creates a workspace:
- Creates a temp directory for the session
- Clones the repository using the user's GitHub PAT
- Creates a new branch for the session
- Mise bootstraps the environment:
- Installs runtime versions from
.nvmrc,.tool-versions, etc. - Installs dependencies via
mise prepare(npm, pip, etc.)
- Installs runtime versions from
- Agent executor runs the selected agent:
- Injects credentials (API keys or subscription tokens)
- Spawns the agent CLI via
mise execfor correct versions - Each provider's
execute()is an async generator yielding typedStreamEventvariants - The stream handler accumulates events into
Partobjects, persists to themessagesandmessage_partstables
- Output streams to the UI via Server-Sent Events:
SessionEventobjects are broadcast using thetypefield as the SSE event name- On reconnect, all messages and parts are replayed from the database
- Clients de-duplicate by part ID
- On completion:
- Commits changes to the branch
- Pushes to the remote repository
- Optionally creates a pull request
- Sandbox is cleaned up
Directory Structure
Sandbox Directory Structure
Each session runs in complete isolation. The sandbox contains:
- A fresh clone of the repository
- mise installation with project-specific tool versions
- Agent credential files (if using subscriptions)
- Environment variables for API keys
When using the sesame-sandbox Docker image, the .mise directory symlinks to pre-installed binaries and shims at /opt/mise/, making common runtimes instantly available without download.
Messages + Parts Model
All real-time data flows through a Messages + Parts model. Messages represent user or assistant turns, and parts represent individual content units within a message (text, tool calls, reasoning, etc.). This is exposed to clients via SessionEvent objects over SSE.
Event Pipeline
Each provider's execute() method is an async generator that parses raw agent output into typed StreamEvent variants via a pure parseLine() function, then yields them. The session executor consumes events with a for await loop and passes each to the stream handler closure.
SessionEvent
SSE events are discriminated by a type field, which is also used as the SSE event name:
| Type | Description |
|---|---|
message.updated | Message metadata created or updated (contains info: Message) |
message.part.updated | Part content created or updated (contains part: Part, optional delta) |
message.removed | Message removed from session |
message.part.removed | Part removed from message |
session.updated | Session status change (contains sessionId, status, optional error) |
log | Sesame-specific log entry (logType: info, command, error, success) |
Part Types
Parts are discriminated by a type field:
| Type | Description |
|---|---|
text | Text content from the agent |
tool | Tool call with lifecycle state (pending → running → completed/error) |
reasoning | Agent's internal reasoning/thinking |
step-start | Marker for the start of a processing step |
step-finish | Step completion with cost, token counts, and finish reason |
file | File attachment (mime type, URL, optional filename) |
StreamEvent Protocol
Each agent provider's execute() is an async generator yielding StreamEvent variants — a typed discriminated union defined in packages/agents/src/types/agent.ts:
| Event | Description |
|---|---|
message-start | New assistant message (contains agent session ID) |
text-delta | Incremental text content |
reasoning-delta | Incremental reasoning/thinking content |
tool-start | Tool call initiated (contains toolCallId, toolName) |
tool-input-delta | Incremental tool input JSON |
tool-result | Tool call completed (contains output, optional isError) |
message-end | Message complete (optional token usage) |
error | Error occurred |
Each provider has a pure parseLine() function in its events.ts that converts raw agent output lines into StreamEvent[]. The createStreamHandler() closure in apps/server/src/services/session-stream-handler.ts consumes these events, accumulates stateful parts (text, tool lifecycle, reasoning), persists to DB, and broadcasts via SSE.
SSE Replay
When a client connects or reconnects, the stream route:
- Reads all messages and message_parts from the database for the session
- Sends
message.updatedandmessage.part.updatedevents for each - Clients de-duplicate by part ID to avoid showing duplicate content
Dynamic Port Forwarding
Sesame automatically detects and exposes ports when dev servers start, allowing you to preview running applications directly in the UI.
How It Works
Framework Detection
The port detection system recognizes output patterns from common frameworks:
| Framework | Example Output | Confidence |
|---|---|---|
| Vite | Local: http://localhost:5173/ | High |
| Next.js | - Local: http://localhost:3000 | High |
| Remix | [remix-serve] http://localhost:3000 | High |
| Astro | Local http://localhost:4321 | High |
| Nuxt | Nuxt Local: http://localhost:3000 | High |
| Bun | Bun v1.x at http://localhost:3000 | High |
| Express | express listening on port 3000 | Medium |
| Generic | http://localhost:8080 | Medium |
Provider Implementations
Local Provider: Ports are directly accessible on localhost - no forwarding needed.
Docker Provider: Uses a pre-allocated port pool with socat proxies inside the container:
The Docker provider:
- Pre-allocates a pool of host ports at container startup (default: 10, configurable via
SANDBOX_PORT_POOL_SIZE) - Maps each pool slot to an internal proxy port (39000+)
- Uses socat inside the container for dynamic forwarding without container restart
- Reclaims pool slots when ports are unexposed
Manual Port Exposure
If automatic detection fails, users can manually expose ports via the Preview pane dropdown menu. This is useful for:
- Services that don't output their port in a detectable format
- Non-standard ports or secondary services
- Monorepos running multiple dev servers
Mise Integration
Sesame uses mise for automatic runtime version and dependency management. This ensures agents work with the exact tool versions specified in the project.
How It Works
Runtime Version Detection
Mise automatically reads version specifications from common config files:
| File | Runtime |
|---|---|
.nvmrc, .node-version | Node.js |
.python-version | Python |
.ruby-version | Ruby |
.tool-versions | Multiple (asdf format) |
mise.toml | Multiple (mise native) |
Dependency Installation
The mise prepare command auto-detects and runs the appropriate install command:
| Lockfile | Command |
|---|---|
package-lock.json | npm install |
yarn.lock | yarn install |
pnpm-lock.yaml | pnpm install |
bun.lock | bun install |
requirements.txt | pip install -r requirements.txt |
poetry.lock | poetry install |
Gemfile.lock | bundle install |
go.sum | go mod download |
Sandbox Isolation
- Docker sandboxes (with
sesame-sandboximage):- mise pre-installed at
/usr/local/bin/mise - Common runtimes pre-cached at
/opt/mise/(Node 20/22/24, Python 3.12/3.13/3.14, Go, Rust, Ruby, Bun) - Agent CLIs pre-installed (claude, codex, copilot, gemini, opencode, amp)
- Per-sandbox mise config symlinks to system shims for instant availability
- mise pre-installed at
- Local sandboxes: mise is downloaded per-sandbox to
{workDir}/.mise/bin/mise - Version is pinned in
packages/sandbox/src/mise/bootstrap.tsfor reproducibility
The official sesame-sandbox Docker image is rebuilt weekly to include the latest runtime versions. See Docker Sandbox for details.
Database Schema
| Table | Purpose |
|---|---|
sessions | Session definitions, status, and metadata |
messages | User and assistant messages per session |
message_parts | Content units within messages (text, tool, reasoning, step-start, step-finish, file) |
keys | Encrypted API keys for AI providers |
agent_credentials | Encrypted subscription credentials |
settings | Setting overrides |
system_settings | Global system-wide configuration |
github_token | Encrypted GitHub PAT (singleton) |
Configuration System
Settings are resolved in priority order:
The admin UI at /admin/settings allows editing config file values. Settings locked by environment variables display a lock icon and cannot be changed.
Security Model
OS-Level Sandbox Security
Sesame uses @anthropic-ai/sandbox-runtime to provide OS-level sandboxing on supported platforms (macOS and Linux). This provides defense-in-depth beyond simple process isolation:
Filesystem Restrictions:
- Write access: Limited to project directory and
/tmp - Read denied: Sensitive paths like
~/.ssh,~/.aws,/etc/passwd - Write denied: System directories, credential files
Network Restrictions:
- Allowed domains: Agent-specific APIs (e.g.,
api.anthropic.comfor Claude), declared per-provider vianetworkDomainsproperty - Global allowed: Configurable via admin settings
- Denied domains: Configurable blocklist
Violation Monitoring:
- Real-time detection of sandbox violations
- Streamed to UI via Server-Sent Events
- Stored in session record
Sandbox Configuration
Security can be configured at multiple levels:
| Level | Scope | Configuration |
|---|---|---|
| Admin | All sessions | Default enabled, global allowed/denied domains |
| Per-session | Single session | Enable/disable, additional domains, sandbox provider override |
| Agent | Per agent type | Built-in domain allowlists per agent (declared on AgentProvider.networkDomains) |
Directory Isolation
- Each session runs in a separate directory
- Agent processes are spawned with restricted environment
- Cleanup runs after session completion
Secrets Management
- API keys encrypted at rest (AES-256-GCM)
- GitHub PATs encrypted with
ENCRYPTION_KEY - Credentials injected via environment variables, not files
Authentication
- Default: no auth required (single-user app)
- Optional HTTP Basic Auth via
AUTH_PASSWORDenv var
Error Handling
All API errors follow the RFC 7807 Problem Details format:
{
"type": "urn:sesame:error:not-found",
"title": "Not Found",
"status": 404,
"detail": "Session not found"
}How It Works
Route handlers throw AppError instances (defined in apps/server/src/lib/errors.ts) with factory functions like badRequest(), notFound(), unauthorized(), etc. The global error middleware catches these and returns properly formatted RFC 7807 JSON responses.
Error Types
| Factory Function | Status | URN Type |
|---|---|---|
badRequest() | 400 | urn:sesame:error:bad-request |
unauthorized() | 401 | urn:sesame:error:unauthorized |
forbidden() | 403 | urn:sesame:error:forbidden |
notFound() | 404 | urn:sesame:error:not-found |
conflict() | 409 | urn:sesame:error:conflict |
gone() | 410 | urn:sesame:error:gone |
payloadTooLarge() | 413 | urn:sesame:error:payload-too-large |
unprocessableEntity() | 422 | urn:sesame:error:unprocessable-entity |
internalError() | 500 | urn:sesame:error:internal-error |
Agent Models API
Agent model lists (which models are available for each agent) are fetched from a centralized public API at api.sesame.works. This allows model lists to be updated without releasing a new version of Sesame. The frontend fetches through the Sesame server's proxy endpoints rather than contacting api.sesame.works directly.
How It Works
- The
useAgentModelshook fetches models from/api/external/models(a server-side proxy) via TanStack Query - The server proxies the request to the configured
sesameApi.baseUrl(default:https://api.sesame.works) - Results are cached and deduplicated across components using Jotai atoms
- If the API is unavailable, hardcoded fallback models are used
- Models include metadata like provider, display name, and which model is the default
- Self-hosters can point
SESAME_API_URLto their own API instance
API Response Format
{
"updatedAt": "2025-01-15T00:00:00Z",
"agents": {
"claude": {
"models": [
{ "id": "claude-sonnet-4-5", "name": "Claude Sonnet 4.5", "provider": "anthropic", "default": true },
{ "id": "claude-opus-4-5", "name": "Claude Opus 4.5", "provider": "anthropic" }
]
},
"codex": { "models": [...] },
"copilot": { "models": [...] },
"gemini": { "models": [...] },
"amp": { "models": [...] },
"opencode": { "models": [...] }
}
}Tech Stack
| Component | Technology |
|---|---|
| Runtime | Bun |
| Build System | Turborepo |
| Backend | Hono (apps/server) |
| Frontend | TanStack Router + Vite (apps/web) |
| Database | SQLite via Drizzle ORM |
| Authentication | Hono Basic Auth (optional) |
| UI Components | shadcn/ui + Tailwind CSS |
| Client State | Jotai |
| Server State | TanStack Query |
| Configuration | c12 (multi-format config loader) |
Development Workflow
# Install dependencies
bun install
# Start both apps (via Turborepo)
bun run dev
# Frontend: http://localhost:13530 (proxies /api to backend)
# Backend: http://localhost:13531In development, the Vite dev server proxies /api requests to the Hono backend on port 13531.
Deployment
When built, the Hono server serves both the API and the pre-built static frontend files:
The Docker image bundles everything into a single container that serves both frontend and API from port 13531.