Agach Daemon (internal/daemon/)
Overview
Background agent that onboards with the server, maintains a WebSocket connection, manages Docker image builds, spawns Claude CLI chat sessions, and manages git worktrees.
Domain Types (domain/)
types.go
OnboardingResult— AccessToken, RefreshToken, NodeID, NodeName, ModeProjectInfo— ID, Name, GitURLDaemonState— StateDisconnected, StateOnboarding, StateConnectedBuildID,BuildStatus— pending, building, success, failedDockerBuild— ID, DockerfileSlug, Version, ImageHash, ImageSize, Status, BuildLog, timestamps
ports.go (interfaces)
ServerAuth— RefreshDaemonToken(nodeID, refreshToken) → accessTokenServerOnboarding— CompleteOnboarding(code, nodeName) → OnboardingResultServerConnection— RunWithReconnect(ctx), Send(msg)ProjectFetcher— GetProject(token, projectID) → ProjectInfoChatUploader— UploadJSONL(token, projectID, featureID, sessionID, filePath)
Configuration (config/config.go)
- File:
~/.config/agach/daemon.yml(or AGACH_SERVER_URL env var) - Fields: base_url, node_name
- Env-only: AGACH_ONBOARDING_CODE (6-digit code)
- Derived: SQLitePath() → daemon.db, WebSocketURL() → ws:// or wss://
App Layer (app/)
Main Loop (app.go)
States: Init → Onboarding → Connected → Reconnecting → Stopped
Run(ctx)— onboard → connect WS → handle events until stopped- WebSocket event routing: docker.list, docker.rebuild, docker.logs, docker.prune, chat.start, chat.user_msg, chat.end, chat.ping
Token Store (tokens.go)
- Persists to
~/.config/agach-daemon/tokens.json(0600 permissions) - Fields: access_token, refresh_token, node_id, node_name
Chat Manager (chat.go)
- Spawns Claude CLI with
--output-format stream-json - Captures stdout/stderr, writes JSONL log
- 30-minute idle TTL with 25-minute warning broadcast
- Stats broadcaster every 5 seconds (token counts, cost, duration)
- Session lifecycle: StartSession → SendMessage → EndSession (stop process, upload JSONL)
ChatSessiontracks: ID, FeatureID, ProjectID, ClaudeSessionID, WorktreePath, token counts, model, message count, timestamps
Docker Service (docker.go)
ListImages(ctx)— groups builds by dockerfile slugRebuild(ctx, slug, eventCh)— creates build record, streams eventsGetBuildLogs(ctx, buildID)— returns build logPruneNonLatest(ctx, eventCh)— removes all but latest per slug
Git Service (git.go)
- Cache dir:
~/.cache/agach EnsureWorktree(projectID, gitURL, mainBranch)— clone or fetch+pull- Auth: SSH keys or GITHUB_TOKEN for HTTPS
Client Layer (client/)
AuthClient— POST /api/daemon/refreshOnboardingClient— POST /api/onboarding/complete (error codes: CODE_NOT_FOUND, CODE_EXPIRED, CODE_ALREADY_USED)ProjectClient— GET /api/projects/{projectID}WSClient— WebSocket with exponential backoff reconnect (1s → 30s max)ChatUploadClient— POST multipart JSONL upload
Inbound WebSocket Hub (inbound/ws/)
- Local hub for TUI clients connecting to the daemon
- Handlers: HandleListDockerfiles, HandleRebuild (async), HandleGetLogs, HandlePrune (async)
- Build/prune events forwarded to hub as they occur
- HandlerFunc signature:
func(ctx, daemonws.Message) (daemonws.Message, error)
Outbound SQLite (outbound/sqlite/)
- Database: daemon.db (local)
buildstable: id, dockerfile_slug, version, image_hash, image_size, status, build_log, timestamps- Repository: Create, FindByID, ListByDockerfile, ListAll, UpdateStatus, Delete, DeleteNonLatest
WebSocket Protocol (pkg/daemonws/)
Docker Messages
docker.list/docker.rebuild/docker.logs/docker.prune- Events:
build.event(slug, build_id, status, log),prune.event(slug, removed, total)
Chat Messages
chat.start→ ChatStartRequest (session_id, feature_id, project_id, node_id, resume_session_id)chat.user_msg→ ChatUserMessage (session_id, content)chat.end→ EndSessionchat.ping→ RefreshActivity- Events:
chat.message,chat.stats,chat.end,chat.error,chat.ttl_warning
Init (init.go)
- Parse
-initflag (create default config) - Load config from file or env
- Create SQLite database + migrations
- Create build repository
- Instantiate daemon app
- Signal handling (SIGINT, SIGTERM)
- Run with reconnect logic