How It Works
The slop behind the slop
This page explains Sesame's internal architecture and how tasks 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:
Task Execution Flow
Step-by-Step
- User creates a task with a prompt and selects a repository
- API validates the request and creates a task record
- SandboxProvider creates a workspace:
- Creates a temp directory under
TASK_DIR_BASE - Clones the repository using the user's GitHub PAT
- Creates a new branch for the task
- Creates a temp directory under
- 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 - Raw output is converted to
UniversalEventby a per-provider converter - Events are persisted to the
task_eventstable (append-only, one INSERT per event)
- Output streams to the UI via Server-Sent Events:
- Each event has a monotonic sequence ID (
seq) used as the SSEid:field - Clients can reconnect with
Last-Event-IDto replay missed events - Events are buffered in-memory for fast replay, with DB fallback
- Each event has a monotonic sequence ID (
- On completion:
- Commits changes to the branch
- Pushes to the remote repository
- Optionally creates a pull request
- Sandbox is cleaned up (unless "Keep Alive" is enabled)
Directory Structure
Sandbox Directory Structure
Each task 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.
Event System
All real-time data flows through a unified UniversalEvent schema. This provides a single, consistent event format regardless of which AI agent produced the output.
Event Pipeline
UniversalEvent
Every event has these core fields:
| Field | Type | Description |
|---|---|---|
seq | number | Monotonically increasing sequence ID per task |
kind | string | Event type: message, tool_call, tool_result, thinking, permission_request, permission_response, status, error, log, done |
timestamp | string | ISO 8601 timestamp |
Additional fields are populated based on kind (e.g., textDelta/fullText for messages, toolCall for tool calls, permissionId/decision for permissions).
Per-Provider Converters
Each agent has a converter that extends BaseConverter in packages/agents/src/converters/. The base class provides:
- Text accumulation: Tracks
textDelta→fullTextacross events - Tool call tracking: Auto-closes open tool calls on flush
- Sequence ID generation: Via callback to
EventSequenceManager
Subclasses override convert() for provider-specific normalization. Most agents (Claude, Copilot) use the default logic. Codex and Gemini have custom handling for their streaming formats.
SSE Replay
When a client reconnects, the stream route checks for a Last-Event-ID header:
- In-memory buffer hit: Events after the given
seqare replayed from the ring buffer (fast path) - DB fallback: If the buffer has been evicted, events are loaded from
task_events(slow path) - Fresh connection: Current task status + all persisted events are sent
Append-Only Event Storage
Events are stored in the task_events table with a unique (taskId, seq) index. This replaces the previous tasks.logs JSON column, eliminating the read-modify-write pattern that caused contention under concurrent writes.
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 | ready on http://localhost:13530 | High |
| Remix | [remix-serve] http://localhost:13530 | High |
| Astro | Local http://localhost:4321 | High |
| Bun | Started server http://localhost:13530 | Medium |
| Express | listening on port 13530 | Medium |
| Generic | http://localhost:13530 | 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)
- 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 |
|---|---|
user | User accounts (email, password hash, role) |
session | Active sessions (managed by better-auth) |
account | OAuth account links |
tasks | Task definitions, status, and metadata |
task_events | Append-only event log (UniversalEvent per row) |
task_messages | Agent output and conversation history |
keys | User's encrypted API keys |
agent_credentials | User's encrypted subscription credentials |
settings | User-specific setting overrides |
connectors | MCP server configurations |
audit_logs | User action audit trail |
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) - 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 task record for audit
Sandbox Configuration
Security can be configured at multiple levels:
| Level | Scope | Configuration |
|---|---|---|
| Admin | All tasks | Default enabled, global allowed/denied domains |
| Per-task | Single task | Enable/disable, additional domains |
| Agent | Per agent type | Built-in domain allowlists per agent |
Directory Isolation
- Each task runs in a separate directory under
TASK_DIR_BASE - Agent processes are spawned with restricted environment
- Cleanup runs after task completion (unless "Keep Alive" enabled)
Secrets Management
- API keys encrypted at rest (AES-256)
- Session tokens signed with
BETTER_AUTH_SECRET - GitHub PATs encrypted with
ENCRYPTION_KEY - Credentials injected via environment variables, not files
Authentication
- Sessions managed by better-auth
- OIDC support for enterprise SSO
- Role-based access (user, admin)
- Admin routes protected with
requireAdminmiddleware
Error Handling
All API errors follow the RFC 7807 Problem Details format:
{
"type": "urn:sesame:error:not-found",
"title": "Not Found",
"status": 404,
"detail": "Task 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 |
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/models. This allows model lists to be updated without releasing a new version of Sesame.
How It Works
- The
useAgentModelshook fetches models fromapi.sesame.works/modelsvia TanStack Query - 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
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": [...] },
"opencode": { "source": "models.dev", "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 | better-auth |
| 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.