Replace complex Docker commands with simple Make targets for building, running, and managing the Discord bot container. This makes it easier for developers to get started without memorizing lengthy Docker flags. Also removes outdated CLAUDE.md and adds AGENTS.md files to guide AI agents working on conversation, database, actors, and sandbox modules.
4.2 KiB
Sandbox Module
Manages Daytona sandbox lifecycle and the OpenCode server running inside each sandbox.
Three-Layer Architecture
- DaytonaService (
daytona.ts) — thin wrapper around@daytonaio/sdk. Creates/starts/stops/destroys sandboxes, executes commands, gets preview links. All methods returnEffectwith typed errors. - SandboxProvisioner (
provisioner.ts) — orchestrates sandbox + OpenCode session lifecycle. Handles provision, resume, health checks, send-failure recovery. - ThreadAgentPool (
pool.ts) — per-thread concurrency layer. Wraps provisioner withActorMap<ThreadId>for serialized access per thread. Manages idle timeouts and cleanup loops.
Sandbox Creation Flow
provision() uses Effect.acquireUseRelease:
- acquire:
daytonaService.create()— creates sandbox withImage.base("node:22-bookworm-slim")+ custom setup - use: clones opencode repo, writes auth/config JSON via env vars, starts
opencode serve, waits for health, creates session - release on failure: destroys the sandbox (cleanup), marks session as errored
The discordBotImage in daytona.ts uses Daytona's Image.base().runCommands().workdir() builder — NOT a Dockerfile. It installs git, curl, gh CLI, opencode-ai, and bun globally.
OpenCode Server Communication
OpenCodeClient (opencode-client.ts) uses @effect/platform's HttpClient:
- Each request uses
scopedClient(preview)which prepends the sandbox preview URL and addsx-daytona-preview-tokenheader HttpClient.filterStatusOkauto-rejects non-2xx responses asResponseErrormapErrorshelper convertsHttpClientError+ParseResult.ParseError→OpenCodeClientError- Health polling:
waitForHealthyretries every 2s up tomaxWaitMs / 2000attempts
PreviewAccess — The Connectivity Token
PreviewAccess (defined in types.ts) carries previewUrl + previewToken. It's extracted from Daytona's getPreviewLink(4096) response (port 4096 is OpenCode's serve port). The token may also be embedded in the URL as ?tkn= — parsePreview normalizes this.
PreviewAccess.from(source) factory works with any object having those two fields — used with SandboxHandle, SessionInfo.
Resume Flow (Non-Obvious)
provisioner.resume() does NOT just restart. It:
- Calls
daytonaService.start()(re-starts the stopped Daytona sandbox) - Runs
restartOpenCodeServe— a shell command that pkills old opencode processes and re-launches - Waits for health (120s default)
- Calls
findOrCreateSessionId— tries to find existing session by title (Discord thread <threadId>), creates new if not found - Returns
ResumedorResumeFailed { allowRecreate }—allowRecreate: falsemeans "don't try recreating, something is fundamentally wrong"
Send Failure Classification
classifySendError in provisioner maps HTTP status codes to recovery strategies:
- 404 →
session-missing(session deleted, mark error) - 0 or 5xx →
sandbox-down(pause sandbox for later resume) - body contains "sandbox not found" / "is the sandbox started" →
sandbox-down - anything else →
non-recoverable(no automatic recovery)
ThreadAgentPool — The ActorMap Bridge
ThreadAgentPool creates ActorMap<ThreadId, SessionInfo> with:
idleTimeout: from configsandboxTimeout(default 30min)onIdle: pauses the sandbox and removes the actorload: reads fromSessionStoreon first accesssave: writes toSessionStoreafter state changes
runtime(threadId, stateRef) creates a Runtime object with current/ensure/send/pause/destroy methods. runRuntime submits work to the actor queue via actors.run(threadId, (state) => ...).
Background Cleanup Loop
Forked with Effect.forkScoped on Schedule.spaced(config.cleanupInterval):
- Pauses stale-active sessions (no activity for
sandboxTimeout + graceMinutes) - Destroys expired-paused sessions (paused longer than
pausedTtlMinutes)
Files That Must Change Together
- Adding a new Daytona operation →
daytona.ts+ add error type inerrors.tsif needed - Changing sandbox setup (image, commands) →
daytona.tsimage builder +provisioner.tsexec commands - Adding a new pool operation →
pool.tsinterface + wire intoconversation/services/conversation.ts