Skip to main content

PostgreSQL (pgserve)

Genie embeds pgserve as a persistent database. One PostgreSQL instance runs per machine on port 19642, auto-started on demand. The connection is a lazy singleton — pgserve only starts when something actually needs the database.

How pgserve Works

CLI command needs DB → getConnection() → is port 19642 listening?

                                    ┌─────────┴──────────┐
                                    │ Yes                 │ No
                                    │ Return cached       │ Start pgserve
                                    │ connection          │ Run migrations
                                    │                     │ Return connection
                                    └─────────────────────┘

Connection Details

SettingValue
Default port19642
Host127.0.0.1
Database namegenie
Data directory~/.genie/data/pgserve/
Port lockfile~/.genie/pgserve.port
Migration marker~/.genie/pgserve.migrated

Crash Recovery

On startup, killOrphanedPostgres() reads postmaster.pid from the data directory, verifies the PID is actually a postgres process (not a reused PID), then sends SIGTERM with a 5-second grace period before escalating to SIGKILL.

Schema Overview

The database schema is organized across 10 migration files covering core scheduling, task lifecycle, projects, state management, observability, boards, and an app registry.

Migration 001: Core Schema

Seven tables for the scheduler and observability:
TablePurpose
schedulesCron-like schedule definitions with status (active/paused/deleted)
triggersIndividual fire instances with lease-based claiming
runsExecution attempts with trace IDs and exit codes
heartbeatsWorker liveness tracking (alive/idle/busy/dead)
audit_eventsImmutable log of all state transitions
agent_checkpointsSession resume state (wish, group, phase, context)
machine_snapshotsMachine state snapshots (workers, teams, CPU, memory)

Migration 002: Task Lifecycle

Eleven tables for the full task management system:
TablePurpose
task_typesDynamic pipeline definitions (e.g., “Software Development” with 7 stages)
tasksUnified work entity — human and agent tasks with priority, timeline, blocking
task_actorsPolymorphic assignment (local, genie_os_user, omni_agent)
task_dependenciesEdges between tasks (depends_on, blocks, relates_to)
task_stage_logAudit trail for stage transitions with run traceability
conversationsChat containers (DM or group) linked to entities
conversation_membersMembership-based permissions
messagesAll messages across all conversations
tagsClassification labels with colors
task_tagsJoin table for task-tag relationships
notification_preferencesPer-actor channel config (WhatsApp, Slack, email, etc.)

Migration 003: Projects

Project-level scoping for multi-repo task management. Adds the projects table with tasks.project_id foreign key.

Migration 004: Cleanup

Removes test pollution from production tables — deletes tasks with /tmp/* repo paths and messages with test* sender IDs. Cleans up orphaned test schemas.

Migration 005: PG State (replaces JSON files)

Five tables that replace the legacy JSON file-based state:
TableReplacesPurpose
agents~/.genie/workers.jsonAgent registry with lifecycle state, tmux pane info, provider metadata
agent_templatesReusable agent definitions with role, launcher, prompt
teams~/.genie/teams/*.jsonTeam configs with repo, branch, members, status
mailbox.genie/mailbox/*.jsonDurable message queue with delivery tracking
team_chat.genie/chat/*.jsonlTeam group chat messages

Migration 006: Schema Reconciliation

Fixes schema drift between seed-created tables and migration-created tables for the agents and agent_templates tables.

Migration 007: Observability

Two tables for full session observability:
TablePurpose
sessionsSession metadata — ID, agent, start/end time, project
session_contentComplementary content — assistant text, tool I/O
OTel structured events (cost, tokens, tool success/fail) flow into the existing audit_events table.

Migration 007b: Mailbox/Team Chat Reconciliation

Fixes production schema drift where 005_pg_state was recorded as applied but partially failed. Ensures mailbox and team_chat tables exist with correct schemas.

Migration 008: Boards

Two tables replacing task_types as the primary pipeline mechanism:
TablePurpose
boardsProject-scoped boards with flexible columns, gates, and actions
board_templatesReusable board blueprints (builtin + custom)
Boards are more flexible than task types — each column can have its own gate type (human, agent, human+agent) and an associated action skill.

Migration 009: App Store

Unified item registry for the Genie ecosystem:
TablePurpose
app_storeCentral registry — agents, skills, apps, boards, workflows, stacks, templates, hooks
installed_appsPer-machine installation records
app_versionsVersion history for each registry item

Built-in Task Pipeline

The software task type defines a 7-stage pipeline that maps to Genie skills:
draft → brainstorm → wish → build → review → qa → ship
         /brainstorm   /wish  /work   /review  /qa
Each stage has a gate type (human, agent, or human+agent) that determines whether the stage can auto-advance:
StageGateAuto-advanceSkill
DrafthumanNo
Brainstormhuman+agentNo/brainstorm
WishhumanNo/wish
BuildagentYes/work
ReviewhumanNo/review
QAagentYes/qa
ShiphumanNo

LISTEN/NOTIFY

PostgreSQL’s LISTEN/NOTIFY mechanism provides real-time event streaming without polling:
ChannelFires WhenPayload
genie_trigger_dueNew pending trigger insertedTrigger ID
genie_audit_eventAny audit event loggedentity_type:event_type:entity_id
genie_task_stageTask stage changestask_id:old_stage:new_stage
genie_messageNew message postedconversation_id:message_id:sender_id
genie_task_depDependency added/removedtask_id:added/removed:dep_id:dep_type
The scheduler daemon subscribes to genie_trigger_due for real-time trigger notification, with a 30-second poll fallback as a safety net.

Task Service API

The task service (task-service.ts) provides CRUD operations scoped by repo_path:
// Create a task
const task = await createTask(sql, repoPath, {
  title: "Implement auth middleware",
  priority: "high",
  wishFile: ".genie/wishes/auth-fix/WISH.md",
  groupName: "1",
});

// Assign an actor
await addActor(sql, task.id, {
  actorType: "local",
  actorId: "engineer-1",
}, "assignee");

// Add dependencies
await addDependency(sql, task.id, otherTask.id, "depends_on");

Execution Locking

Tasks support atomic checkout via checkout_run_id and execution_locked_at fields. This prevents two agents from claiming the same task simultaneously:
UPDATE tasks
SET checkout_run_id = $1, execution_locked_at = now()
WHERE id = $2 AND checkout_run_id IS NULL
RETURNING *;
The lock expires after checkout_timeout_ms (default: 600,000ms / 10 minutes).

Migration Runner

Migrations are loaded from src/db/migrations/ in lexicographic order. The _genie_migrations table tracks which migrations have been applied:
CREATE TABLE _genie_migrations (
  id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT UNIQUE NOT NULL,
  applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Each migration runs in its own transaction. If a migration fails, subsequent migrations are skipped and the error is thrown.