diff --git a/.trajectories/completed/2026-01/traj_he75f24d1xfm.json b/.trajectories/completed/2026-01/traj_he75f24d1xfm.json new file mode 100644 index 00000000..15f5d5f7 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_he75f24d1xfm.json @@ -0,0 +1,101 @@ +{ + "id": "traj_he75f24d1xfm", + "version": 1, + "task": { + "title": "Implement cloud message storage for Algolia challenge", + "source": { + "system": "plain", + "id": "algolia-challenge-prep" + } + }, + "status": "completed", + "startedAt": "2026-01-08T23:57:42.804Z", + "agents": [ + { + "name": "khaliqgant", + "role": "lead", + "joinedAt": "2026-01-08T23:57:42.804Z" + } + ], + "chapters": [ + { + "id": "chap_ag0efa57f2sd", + "title": "Work", + "agentName": "default", + "startedAt": "2026-01-08T23:57:49.778Z", + "events": [ + { + "ts": 1767916669779, + "type": "decision", + "content": "Store messages in PostgreSQL with workspace-scoped deduplication: Store messages in PostgreSQL with workspace-scoped deduplication", + "raw": { + "question": "Store messages in PostgreSQL with workspace-scoped deduplication", + "chosen": "Store messages in PostgreSQL with workspace-scoped deduplication", + "alternatives": [], + "reasoning": "Messages need to be searchable via Algolia. Using workspace_id + original_id unique constraint prevents duplicates when daemons sync the same message multiple times." + }, + "significance": "high" + }, + { + "ts": 1767916679801, + "type": "decision", + "content": "Plan-based retention policy with expires_at column: Plan-based retention policy with expires_at column", + "raw": { + "question": "Plan-based retention policy with expires_at column", + "chosen": "Plan-based retention policy with expires_at column", + "alternatives": [], + "reasoning": "Free tier: 30 days, Pro: 90 days, Enterprise: unlimited. Using nullable expires_at column allows easy cleanup queries and different retention per plan." + }, + "significance": "high" + }, + { + "ts": 1767916681658, + "type": "decision", + "content": "Sync messages during heartbeat cycle: Sync messages during heartbeat cycle", + "raw": { + "question": "Sync messages during heartbeat cycle", + "chosen": "Sync messages during heartbeat cycle", + "alternatives": [], + "reasoning": "Daemon already sends heartbeat every 30s to cloud. Adding message sync to this cycle reuses existing infrastructure without adding new timers or connections." + }, + "significance": "high" + }, + { + "ts": 1767916682582, + "type": "decision", + "content": "Track indexedAt for Algolia sync queue: Track indexedAt for Algolia sync queue", + "raw": { + "question": "Track indexedAt for Algolia sync queue", + "chosen": "Track indexedAt for Algolia sync queue", + "alternatives": [], + "reasoning": "Separate indexedAt timestamp allows independent sync to Algolia. Messages can be stored in PostgreSQL first, then batch-indexed to Algolia without blocking the daemon sync." + }, + "significance": "high" + }, + { + "ts": 1767916690475, + "type": "decision", + "content": "Use Drizzle inArray instead of raw SQL ANY: Use Drizzle inArray instead of raw SQL ANY", + "raw": { + "question": "Use Drizzle inArray instead of raw SQL ANY", + "chosen": "Use Drizzle inArray instead of raw SQL ANY", + "alternatives": [], + "reasoning": "Initial implementation used raw SQL ANY syntax which may not work correctly with Drizzle parameterization. Fixed to use Drizzle's type-safe inArray helper for the markIndexed bulk update." + }, + "significance": "high" + } + ], + "endedAt": "2026-01-08T23:58:17.292Z" + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", + "tags": [], + "completedAt": "2026-01-08T23:58:17.292Z", + "retrospective": { + "summary": "Added cloud message storage infrastructure for Algolia challenge. Created agent_messages table with workspace scoping, plan-based retention, and Algolia sync tracking. Extended daemon CloudSyncService to sync messages during heartbeat. Added /api/daemons/messages/sync endpoint. All 1119 tests pass.", + "approach": "Standard approach", + "confidence": 0.9 + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_he75f24d1xfm.md b/.trajectories/completed/2026-01/traj_he75f24d1xfm.md new file mode 100644 index 00000000..89f0bc51 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_he75f24d1xfm.md @@ -0,0 +1,52 @@ +# Trajectory: Implement cloud message storage for Algolia challenge + +> **Status:** ✅ Completed +> **Task:** algolia-challenge-prep +> **Confidence:** 90% +> **Started:** January 9, 2026 at 12:57 AM +> **Completed:** January 9, 2026 at 12:58 AM + +--- + +## Summary + +Added cloud message storage infrastructure for Algolia challenge. Created agent_messages table with workspace scoping, plan-based retention, and Algolia sync tracking. Extended daemon CloudSyncService to sync messages during heartbeat. Added /api/daemons/messages/sync endpoint. All 1119 tests pass. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Store messages in PostgreSQL with workspace-scoped deduplication +- **Chose:** Store messages in PostgreSQL with workspace-scoped deduplication +- **Reasoning:** Messages need to be searchable via Algolia. Using workspace_id + original_id unique constraint prevents duplicates when daemons sync the same message multiple times. + +### Plan-based retention policy with expires_at column +- **Chose:** Plan-based retention policy with expires_at column +- **Reasoning:** Free tier: 30 days, Pro: 90 days, Enterprise: unlimited. Using nullable expires_at column allows easy cleanup queries and different retention per plan. + +### Sync messages during heartbeat cycle +- **Chose:** Sync messages during heartbeat cycle +- **Reasoning:** Daemon already sends heartbeat every 30s to cloud. Adding message sync to this cycle reuses existing infrastructure without adding new timers or connections. + +### Track indexedAt for Algolia sync queue +- **Chose:** Track indexedAt for Algolia sync queue +- **Reasoning:** Separate indexedAt timestamp allows independent sync to Algolia. Messages can be stored in PostgreSQL first, then batch-indexed to Algolia without blocking the daemon sync. + +### Use Drizzle inArray instead of raw SQL ANY +- **Chose:** Use Drizzle inArray instead of raw SQL ANY +- **Reasoning:** Initial implementation used raw SQL ANY syntax which may not work correctly with Drizzle parameterization. Fixed to use Drizzle's type-safe inArray helper for the markIndexed bulk update. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Store messages in PostgreSQL with workspace-scoped deduplication: Store messages in PostgreSQL with workspace-scoped deduplication +- Plan-based retention policy with expires_at column: Plan-based retention policy with expires_at column +- Sync messages during heartbeat cycle: Sync messages during heartbeat cycle +- Track indexedAt for Algolia sync queue: Track indexedAt for Algolia sync queue +- Use Drizzle inArray instead of raw SQL ANY: Use Drizzle inArray instead of raw SQL ANY diff --git a/.trajectories/index.json b/.trajectories/index.json index fc0b0776..9b43f1df 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-01-08T09:02:38.297Z", + "lastUpdated": "2026-01-08T23:58:17.303Z", "trajectories": { "traj_ozd98si6a7ns": { "title": "Fix thinking indicator showing on all messages", @@ -526,6 +526,13 @@ "startedAt": "2026-01-08T09:02:29.285Z", "completedAt": "2026-01-08T09:02:38.286Z", "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json" + }, + "traj_he75f24d1xfm": { + "title": "Implement cloud message storage for Algolia challenge", + "status": "completed", + "startedAt": "2026-01-08T23:57:42.804Z", + "completedAt": "2026-01-08T23:58:17.292Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_he75f24d1xfm.json" } } } \ No newline at end of file diff --git a/docs/specs/algolia-agent-memory-spec.md b/docs/specs/algolia-agent-memory-spec.md new file mode 100644 index 00000000..d189f3df --- /dev/null +++ b/docs/specs/algolia-agent-memory-spec.md @@ -0,0 +1,807 @@ +# Algolia Agent Memory: Implementation Specification + +> **Competition**: Algolia Agent Studio Challenge +> **Category**: Non-Conversational Consumer-Facing Experience +> **Deadline**: February 8, 2026 +> **Prize**: $750 per winner + +--- + +## Executive Summary + +**Agent Memory** is a searchable knowledge base of agent work history, powered by Algolia. It enables developers and teams to instantly search across all agent conversations, decisions, tasks, and code changes—surfacing relevant context without requiring a conversation. + +**Differentiator**: No one else has this corpus. We're indexing the *reasoning* behind code, not just the code itself. + +--- + +## Product Vision + +### The Problem + +When agents work on codebases, valuable context is lost: +- Why was this approach chosen over alternatives? +- Who worked on similar problems before? +- What decisions were made during implementation? +- What conversations led to this code? + +### The Solution + +A non-conversational search interface that proactively surfaces relevant agent work history: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 🔍 Search Agent History [Algolia-powered]│ +├─────────────────────────────────────────────────────────────────────┤ +│ "authentication implementation" │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 📋 Task: Add OAuth login with GitHub │ +│ Agent: Alice • Completed 3 days ago • Priority: High │ +│ "Implemented OAuth flow with refresh tokens..." │ +│ │ +│ 💭 Decision: JWT over sessions │ +│ Confidence: 85% • Agent: Alice │ +│ "Chose JWT for stateless scaling requirements" │ +│ │ +│ 💬 Conversation Thread │ +│ Alice → Bob: "Let's use refresh tokens for security" │ +│ Bob → Alice: "ACK, updating middleware now" │ +│ │ +│ 📝 Commit: e02d849 │ +│ "Add JWT auth with refresh token support" │ +│ Files: src/auth/jwt.ts, src/middleware/auth.ts │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Technical Architecture + +### Data Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ DATA SOURCES │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Messages │ │ Beads │ │ Trails │ │ +│ │ (relay) │ │ (tasks) │ │ (decisions) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └────────────┬────┴────────────────┘ │ +│ ▼ │ +│ ┌────────────────────────┐ │ +│ │ PostgreSQL Cloud │ │ +│ │ (agent_messages) │ │ +│ └────────────┬───────────┘ │ +│ │ │ +└──────────────────────┼──────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ALGOLIA INDEXER │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Indexer Service (cron job / webhook triggered) │ │ +│ │ │ │ +│ │ 1. Query agent_messages WHERE indexed_at IS NULL │ │ +│ │ 2. Query beads via .beads/issues.jsonl │ │ +│ │ 3. Query trails via .trajectories/ │ │ +│ │ 4. Transform to unified AgentWorkRecord schema │ │ +│ │ 5. Batch push to Algolia │ │ +│ │ 6. Mark records as indexed │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ALGOLIA INDEX │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Index: agent-work-history │ +│ │ +│ Searchable Attributes: │ +│ - title (task title, commit message, decision summary) │ +│ - content (full text body) │ +│ - agent (who created this) │ +│ - files[] (related file paths) │ +│ │ +│ Facets: │ +│ - type (message, bead, trail, commit) │ +│ - status (open, closed, completed) │ +│ - agent │ +│ - confidence (for decisions) │ +│ │ +│ Custom Ranking: │ +│ - timestamp (desc) - recent first │ +│ - confidence (desc) - high confidence first │ +│ - priority (asc) - high priority first │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ DASHBOARD UI │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ React Component: AgentMemorySearch │ │ +│ │ │ │ +│ │ - InstantSearch integration │ │ +│ │ - Faceted filtering by type, agent, status │ │ +│ │ - Highlighted search results │ │ +│ │ - Links to source (bead detail, trail viewer, thread) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ React Component: RelatedContextPanel │ │ +│ │ │ │ +│ │ - Proactive suggestions (non-conversational) │ │ +│ │ - Shows related work when viewing agent/task │ │ +│ │ - "Similar past work", "Related decisions" │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Data Schema + +### Unified Index Record + +```typescript +interface AgentWorkRecord { + // Algolia required + objectID: string; // Unique ID: "{type}_{source_id}" + + // Core searchable content + title: string; // Primary display text + content: string; // Full searchable body + + // Record classification + type: 'message' | 'bead' | 'trail' | 'commit'; + + // Temporal + timestamp: number; // Unix timestamp (ms) + + // Attribution + workspaceId: string; // Workspace scope + agent?: string; // Agent who created this + + // Context + thread?: string; // Conversation thread ID + files?: string[]; // Related file paths + + // Task linkage + beadId?: string; // Link to bead (task) + trajectoryId?: string; // Link to trail + commitSha?: string; // Link to git commit + + // For ranking/filtering + status?: 'open' | 'in_progress' | 'completed' | 'closed'; + priority?: number; // 0-4 (0 = critical) + confidence?: number; // 0-100 for decisions + significance?: 'low' | 'medium' | 'high'; + + // PDERO phase (for trails) + phase?: 'problem' | 'design' | 'execution' | 'review' | 'optimize'; +} +``` + +### Source-Specific Mappings + +#### Messages → AgentWorkRecord + +```typescript +function messageToRecord(msg: AgentMessage): AgentWorkRecord { + return { + objectID: `message_${msg.id}`, + title: `${msg.fromAgent} → ${msg.toAgent}`, + content: msg.body, + type: 'message', + timestamp: msg.messageTs.getTime(), + workspaceId: msg.workspaceId, + agent: msg.fromAgent, + thread: msg.thread, + status: msg.isBroadcast ? undefined : 'completed', + }; +} +``` + +#### Beads → AgentWorkRecord + +```typescript +function beadToRecord(bead: BeadIssue): AgentWorkRecord { + return { + objectID: `bead_${bead.id}`, + title: bead.title, + content: bead.description || '', + type: 'bead', + timestamp: new Date(bead.created_at).getTime(), + workspaceId: bead.workspace_id, + agent: bead.assignee, + beadId: bead.id, + status: bead.status, + priority: bead.priority, + }; +} +``` + +#### Trails → AgentWorkRecord + +```typescript +function trailToRecord(trail: Trajectory): AgentWorkRecord { + const decisions = trail.chapters + .flatMap(c => c.events) + .filter(e => e.type === 'decision'); + + return { + objectID: `trail_${trail.id}`, + title: trail.task, + content: [ + trail.retrospective?.summary, + ...decisions.map(d => `${d.content}: ${d.reasoning}`), + ].filter(Boolean).join('\n'), + type: 'trail', + timestamp: new Date(trail.completedAt).getTime(), + workspaceId: trail.workspaceId, + agent: trail.agents?.[0], + trajectoryId: trail.id, + confidence: trail.retrospective?.confidence + ? Math.round(trail.retrospective.confidence * 100) + : undefined, + phase: trail.currentPhase, + }; +} +``` + +--- + +## Implementation Plan + +### Phase 1: Algolia Setup (Day 1) + +**Objective**: Create and configure Algolia index + +#### Tasks + +1. **Create Algolia Account** + - Sign up for Algolia (Free Build Plan: 10K records, 10K searches/month) + - Create application: `agent-relay-memory` + +2. **Create Index** + - Index name: `agent-work-history` + - Configure searchable attributes: + ``` + title + content + agent + files + ``` + - Configure facets: + ``` + type + status + agent + phase + ``` + - Configure custom ranking: + ``` + desc(timestamp) + desc(confidence) + asc(priority) + ``` + +3. **Generate API Keys** + - Admin key (for indexing) → store in `ALGOLIA_ADMIN_KEY` + - Search-only key (for frontend) → store in `ALGOLIA_SEARCH_KEY` + +#### Deliverables +- [ ] Algolia account created +- [ ] Index configured with proper settings +- [ ] API keys stored in environment + +--- + +### Phase 2: Indexer Service (Days 2-3) + +**Objective**: Build service to sync data to Algolia + +#### File Structure + +``` +src/cloud/services/algolia/ +├── index.ts # Main exports +├── client.ts # Algolia client setup +├── indexer.ts # Main indexer service +├── transformers/ +│ ├── messages.ts # Message → AgentWorkRecord +│ ├── beads.ts # Bead → AgentWorkRecord +│ └── trails.ts # Trail → AgentWorkRecord +└── types.ts # AgentWorkRecord type +``` + +#### Core Implementation + +```typescript +// src/cloud/services/algolia/indexer.ts + +import algoliasearch from 'algoliasearch'; +import { db } from '../../db/index.js'; +import { transformMessage } from './transformers/messages.js'; +import type { AgentWorkRecord } from './types.js'; + +export class AlgoliaIndexer { + private client: ReturnType; + private index: ReturnType['initIndex']>; + + constructor() { + this.client = algoliasearch( + process.env.ALGOLIA_APP_ID!, + process.env.ALGOLIA_ADMIN_KEY! + ); + this.index = this.client.initIndex('agent-work-history'); + } + + /** + * Sync unindexed messages to Algolia + */ + async syncMessages(workspaceId: string, batchSize = 100): Promise { + const messages = await db.agentMessages.getUnindexed(workspaceId, batchSize); + + if (messages.length === 0) return 0; + + const records: AgentWorkRecord[] = messages.map(transformMessage); + + await this.index.saveObjects(records); + await db.agentMessages.markIndexed(messages.map(m => m.id)); + + return records.length; + } + + /** + * Full reindex of a workspace + */ + async reindexWorkspace(workspaceId: string): Promise { + // Clear existing records for workspace + await this.index.deleteBy({ + filters: `workspaceId:${workspaceId}`, + }); + + // Reindex all sources + await this.syncMessages(workspaceId, 500); + // await this.syncBeads(workspaceId); + // await this.syncTrails(workspaceId); + } + + /** + * Search the index + */ + async search( + workspaceId: string, + query: string, + options?: { filters?: string; facets?: string[] } + ) { + return this.index.search(query, { + filters: `workspaceId:${workspaceId}${options?.filters ? ` AND ${options.filters}` : ''}`, + facets: options?.facets || ['type', 'agent', 'status'], + attributesToHighlight: ['title', 'content'], + highlightPreTag: '', + highlightPostTag: '', + }); + } +} +``` + +#### API Endpoints + +```typescript +// src/cloud/api/search.ts + +import { Router, Request, Response } from 'express'; +import { requireAuth } from './auth.js'; +import { AlgoliaIndexer } from '../services/algolia/indexer.js'; + +export const searchRouter = Router(); +const indexer = new AlgoliaIndexer(); + +/** + * GET /api/search + * Search agent work history + */ +searchRouter.get('/', requireAuth, async (req: Request, res: Response) => { + const { q, type, agent, status } = req.query; + const workspaceId = (req as any).user.workspaceId; + + const filters = [ + type && `type:${type}`, + agent && `agent:${agent}`, + status && `status:${status}`, + ].filter(Boolean).join(' AND '); + + const results = await indexer.search(workspaceId, q as string, { filters }); + + res.json(results); +}); + +/** + * POST /api/search/reindex + * Trigger full reindex (admin only) + */ +searchRouter.post('/reindex', requireAuth, async (req: Request, res: Response) => { + const workspaceId = (req as any).user.workspaceId; + + await indexer.reindexWorkspace(workspaceId); + + res.json({ success: true }); +}); +``` + +#### Deliverables +- [ ] AlgoliaIndexer class implemented +- [ ] Message transformer implemented +- [ ] API endpoints for search and reindex +- [ ] Cron job for periodic sync (every 5 min) + +--- + +### Phase 3: Dashboard Integration (Days 4-5) + +**Objective**: Build search UI in dashboard + +#### Components + +``` +src/dashboard/react-components/ +├── search/ +│ ├── AgentMemorySearch.tsx # Main search interface +│ ├── SearchResultCard.tsx # Individual result display +│ ├── SearchFacets.tsx # Filter sidebar +│ └── RelatedContextPanel.tsx # Proactive suggestions +``` + +#### Main Search Component + +```tsx +// src/dashboard/react-components/search/AgentMemorySearch.tsx + +import React, { useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; + +interface SearchResult { + objectID: string; + title: string; + content: string; + type: 'message' | 'bead' | 'trail' | 'commit'; + agent?: string; + timestamp: number; + _highlightResult?: { + title?: { value: string }; + content?: { value: string }; + }; +} + +export function AgentMemorySearch() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [facets, setFacets] = useState>>({}); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({ type: '', agent: '', status: '' }); + + const handleSearch = useCallback(async (q: string) => { + if (!q.trim()) { + setResults([]); + return; + } + + setLoading(true); + try { + const params = new URLSearchParams({ q, ...filters }); + const res = await fetch(`/api/search?${params}`); + const data = await res.json(); + setResults(data.hits); + setFacets(data.facets); + } finally { + setLoading(false); + } + }, [filters]); + + return ( +
+ {/* Search Header */} +
+
+ { + setQuery(e.target.value); + handleSearch(e.target.value); + }} + placeholder="Search agent work history..." + className="w-full px-4 py-3 pl-10 bg-bg-primary border border-border-subtle + rounded-lg text-text-primary placeholder-text-muted + focus:outline-none focus:border-accent-cyan focus:ring-1 focus:ring-accent-cyan" + /> + +
+
+ +
+ {/* Facets Sidebar */} +
+ +
+ + {/* Results */} +
+ {loading ? ( +
Searching...
+ ) : results.length === 0 ? ( +
+ {query ? 'No results found' : 'Search for agent conversations, tasks, and decisions'} +
+ ) : ( +
+ {results.map((result) => ( + + ))} +
+ )} +
+
+
+ ); +} +``` + +#### Result Card Component + +```tsx +// src/dashboard/react-components/search/SearchResultCard.tsx + +import React from 'react'; + +const TYPE_ICONS = { + message: '💬', + bead: '📋', + trail: '🛤️', + commit: '📝', +}; + +const TYPE_COLORS = { + message: 'bg-blue-500/10 text-blue-400', + bead: 'bg-green-500/10 text-green-400', + trail: 'bg-purple-500/10 text-purple-400', + commit: 'bg-orange-500/10 text-orange-400', +}; + +export function SearchResultCard({ result }: { result: SearchResult }) { + const timeAgo = formatTimeAgo(result.timestamp); + + return ( +
+ {/* Header */} +
+ + {TYPE_ICONS[result.type]} {result.type} + + {result.agent && ( + + by {result.agent} + + )} + + {timeAgo} + +
+ + {/* Title */} +

+ + {/* Content Preview */} +

+

+ ); +} +``` + +#### Deliverables +- [ ] AgentMemorySearch component +- [ ] SearchResultCard component +- [ ] SearchFacets component +- [ ] RelatedContextPanel component (proactive suggestions) +- [ ] Integration with dashboard routing + +--- + +### Phase 4: Agent Studio Integration (Day 6) + +**Objective**: Create Algolia Agent for conversational queries + +#### Agent Configuration + +In Algolia Agent Studio dashboard: + +1. **Create Agent**: "Agent Memory Assistant" + +2. **System Prompt**: + ``` + You are an Agent Memory Assistant for a software development team. + You help developers find relevant past work, decisions, and conversations + from the agent work history. + + When answering questions: + - Search for relevant messages, tasks (beads), and decisions (trails) + - Cite specific sources with their type and timestamp + - Highlight confidence levels for decisions + - Suggest related context the user might find useful + + Focus on the "why" behind code, not just the "what". + ``` + +3. **Tools**: Enable Algolia Search on `agent-work-history` index + +4. **Integration**: + ```typescript + // Optional: Add chat interface using AI SDK + import { useChat } from "@ai-sdk/react"; + + const { messages, sendMessage } = useChat({ + api: `https://${ALGOLIA_APP_ID}.algolia.net/agent-studio/1/agents/${AGENT_ID}/completions?stream=true`, + headers: { + 'x-algolia-application-id': ALGOLIA_APP_ID, + 'x-algolia-api-key': ALGOLIA_SEARCH_KEY, + } + }); + ``` + +#### Deliverables +- [ ] Agent created in Agent Studio +- [ ] System prompt configured +- [ ] Optional chat UI component + +--- + +### Phase 5: Polish & Demo (Day 7) + +**Objective**: Prepare for competition submission + +#### Tasks + +1. **Seed Demo Data** + - Populate with realistic agent conversations + - Add variety of beads (tasks) with different statuses + - Create trails with decisions and confidence scores + +2. **Record Demo Video** + - Show search across different data types + - Demonstrate faceted filtering + - Highlight proactive suggestions + - Show Agent Studio conversation (if implemented) + +3. **Write Submission** + - Title: "Agent Memory: Search Your AI Team's Work History" + - Category: Non-Conversational + - Description emphasizing: + - Novel corpus (agent reasoning, not just code) + - Real-time sync from distributed daemons + - Proactive context surfacing + - Plan-based retention policies + +#### Deliverables +- [ ] Demo data populated +- [ ] Demo video recorded (2-3 min) +- [ ] DEV.to submission posted + +--- + +## Judging Criteria Alignment + +| Criterion | How We Score | +|-----------|--------------| +| **Use of Algolia Technology** | Full pipeline: Real data → Algolia index → InstantSearch UI + optional Agent Studio | +| **User Experience** | Non-conversational proactive intelligence; instant search with facets and highlighting | +| **Originality** | First "agent memory" system—indexing reasoning and decisions, not just code | + +--- + +## Risk Mitigation + +| Risk | Mitigation | +|------|------------| +| Free tier limits (10K records) | Focus on recent data; implement aggressive dedup | +| No demo data | Create seed script with realistic conversations | +| Complex setup | Document all steps; use environment variables | +| Competition entry quality | Focus on polish; record professional demo video | + +--- + +## Success Metrics + +- [ ] Search returns results in <100ms +- [ ] All 4 data types (message, bead, trail, commit) searchable +- [ ] Faceted filtering works correctly +- [ ] Highlighting shows relevant matches +- [ ] Demo video clearly shows value proposition + +--- + +## Timeline + +| Day | Focus | Deliverables | +|-----|-------|--------------| +| 1 | Algolia Setup | Account, index, API keys | +| 2-3 | Indexer Service | AlgoliaIndexer, transformers, API | +| 4-5 | Dashboard UI | Search components, integration | +| 6 | Agent Studio | Optional chat agent | +| 7 | Polish & Submit | Demo video, submission | + +--- + +## Appendix: Environment Variables + +```bash +# Algolia Configuration +ALGOLIA_APP_ID=your_app_id +ALGOLIA_ADMIN_KEY=your_admin_key # Server-side only +ALGOLIA_SEARCH_KEY=your_search_key # Safe for frontend + +# Index Configuration +ALGOLIA_INDEX_NAME=agent-work-history +``` + +--- + +## Appendix: Algolia Index Settings + +```json +{ + "searchableAttributes": [ + "title", + "content", + "agent", + "files" + ], + "attributesForFaceting": [ + "filterOnly(workspaceId)", + "type", + "status", + "agent", + "phase" + ], + "customRanking": [ + "desc(timestamp)", + "desc(confidence)", + "asc(priority)" + ], + "attributesToHighlight": [ + "title", + "content" + ], + "highlightPreTag": "", + "highlightPostTag": "", + "hitsPerPage": 20 +} +``` diff --git a/src/cloud/api/daemons.ts b/src/cloud/api/daemons.ts index 484344c1..4ae84367 100644 --- a/src/cloud/api/daemons.ts +++ b/src/cloud/api/daemons.ts @@ -446,3 +446,151 @@ daemonsRouter.get('/messages', requireDaemonAuth as any, async (req: Request, re res.status(500).json({ error: 'Failed to fetch messages' }); } }); + +// ============================================================================ +// Message Sync API (sync daemon SQLite messages to cloud PostgreSQL) +// ============================================================================ + +/** + * Sync message input from daemon + */ +interface SyncMessageInput { + id: string; // Original message ID from daemon SQLite + ts: number; // Timestamp in ms + from: string; // From agent name + to: string; // To agent name or '*' for broadcast + body: string; // Message body + kind?: string; // message, action, state, thinking + topic?: string; + thread?: string; + channel?: string; + is_broadcast?: boolean; + is_urgent?: boolean; + data?: Record; + payload_meta?: { + requires_ack?: boolean; + ttl_ms?: number; + importance?: number; + replyTo?: string; + }; +} + +/** + * POST /api/daemons/messages/sync + * Sync messages from daemon to cloud storage + * + * Accepts batches of messages and stores them in agent_messages table. + * Uses upsert logic to handle duplicates (based on workspace_id + original_id). + * + * Request body: + * { + * messages: SyncMessageInput[] + * } + * + * Response: + * { + * success: true, + * synced: number, // Count of messages synced + * duplicates: number // Count of messages skipped (already existed) + * } + */ +daemonsRouter.post('/messages/sync', requireDaemonAuth as any, async (req: Request, res: Response) => { + const daemon = (req as any).daemon; + const { messages } = req.body as { messages: SyncMessageInput[] }; + + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ error: 'messages array is required' }); + } + + if (messages.length === 0) { + return res.json({ success: true, synced: 0, duplicates: 0 }); + } + + // Limit batch size to prevent abuse + if (messages.length > 500) { + return res.status(400).json({ error: 'Maximum batch size is 500 messages' }); + } + + // Require workspace to be linked + if (!daemon.workspaceId) { + return res.status(400).json({ + error: 'Daemon must be linked to a workspace to sync messages. Re-link with a workspace ID.', + }); + } + + try { + // Get user plan to determine retention policy + const user = await db.users.findById(daemon.userId); + const plan = user?.plan || 'free'; + + // Calculate expires_at based on plan + let expiresAt: Date | null = null; + if (plan === 'free') { + expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days + } else if (plan === 'pro') { + expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days + } + // Enterprise: null (never expires) + + // Transform to NewAgentMessage format + const dbMessages = messages.map((msg) => ({ + workspaceId: daemon.workspaceId, + daemonId: daemon.id, + originalId: msg.id, + fromAgent: msg.from, + toAgent: msg.to, + body: msg.body, + kind: msg.kind || 'message', + topic: msg.topic || null, + thread: msg.thread || null, + channel: msg.channel || null, + isBroadcast: msg.is_broadcast || msg.to === '*', + isUrgent: msg.is_urgent || false, + data: msg.data || null, + payloadMeta: msg.payload_meta || null, + messageTs: new Date(msg.ts), + expiresAt, + })); + + // Insert with onConflictDoNothing for deduplication + const inserted = await db.agentMessages.createMany(dbMessages); + + const synced = inserted.length; + const duplicates = messages.length - synced; + + console.log(`[message-sync] Synced ${synced} messages for daemon ${daemon.id}, ${duplicates} duplicates skipped`); + + res.json({ + success: true, + synced, + duplicates, + }); + } catch (error) { + console.error('Error syncing messages:', error); + res.status(500).json({ error: 'Failed to sync messages' }); + } +}); + +/** + * GET /api/daemons/messages/stats + * Get message sync statistics for this daemon's workspace + */ +daemonsRouter.get('/messages/stats', requireDaemonAuth as any, async (req: Request, res: Response) => { + const daemon = (req as any).daemon; + + if (!daemon.workspaceId) { + return res.status(400).json({ error: 'Daemon must be linked to a workspace' }); + } + + try { + const count = await db.agentMessages.countByWorkspace(daemon.workspaceId); + + res.json({ + workspaceId: daemon.workspaceId, + messageCount: count, + }); + } catch (error) { + console.error('Error fetching message stats:', error); + res.status(500).json({ error: 'Failed to fetch message stats' }); + } +}); diff --git a/src/cloud/db/drizzle.ts b/src/cloud/db/drizzle.ts index 3fc5a5c6..cb3ae371 100644 --- a/src/cloud/db/drizzle.ts +++ b/src/cloud/db/drizzle.ts @@ -7,7 +7,7 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; -import { eq, and, sql, desc, lt, isNull, isNotNull } from 'drizzle-orm'; +import { eq, and, sql, desc, lt, isNull, isNotNull, inArray } from 'drizzle-orm'; import * as schema from './schema.js'; import { getConfig } from '../config.js'; @@ -1583,6 +1583,186 @@ export const commentMentionQueries: CommentMentionQueries = { }, }; +// ============================================================================ +// Agent Message Queries +// ============================================================================ + +export interface MessageQuery { + workspaceId: string; + limit?: number; + offset?: number; + fromAgent?: string; + toAgent?: string; + thread?: string; + channel?: string; + sinceTs?: Date; + beforeTs?: Date; + includeExpired?: boolean; +} + +export interface AgentMessageQueries { + create(data: schema.NewAgentMessage): Promise; + createMany(data: schema.NewAgentMessage[]): Promise; + findById(id: string): Promise; + findByOriginalId(workspaceId: string, originalId: string): Promise; + query(params: MessageQuery): Promise; + getUnindexed(workspaceId: string, limit?: number): Promise; + markIndexed(ids: string[]): Promise; + deleteExpired(): Promise; + countByWorkspace(workspaceId: string): Promise; + getThreadMessages(workspaceId: string, thread: string, limit?: number): Promise; +} + +export const agentMessageQueries: AgentMessageQueries = { + async create(data: schema.NewAgentMessage): Promise { + const db = getDb(); + const result = await db.insert(schema.agentMessages).values(data).returning(); + return result[0]; + }, + + async createMany(data: schema.NewAgentMessage[]): Promise { + if (data.length === 0) return []; + const db = getDb(); + const result = await db + .insert(schema.agentMessages) + .values(data) + .onConflictDoNothing() // Skip duplicates based on workspace_original_unique constraint + .returning(); + return result; + }, + + async findById(id: string): Promise { + const db = getDb(); + const result = await db + .select() + .from(schema.agentMessages) + .where(eq(schema.agentMessages.id, id)); + return result[0] ?? null; + }, + + async findByOriginalId(workspaceId: string, originalId: string): Promise { + const db = getDb(); + const result = await db + .select() + .from(schema.agentMessages) + .where( + and( + eq(schema.agentMessages.workspaceId, workspaceId), + eq(schema.agentMessages.originalId, originalId) + ) + ); + return result[0] ?? null; + }, + + async query(params: MessageQuery): Promise { + const db = getDb(); + const conditions = [eq(schema.agentMessages.workspaceId, params.workspaceId)]; + + if (params.fromAgent) { + conditions.push(eq(schema.agentMessages.fromAgent, params.fromAgent)); + } + if (params.toAgent) { + conditions.push(eq(schema.agentMessages.toAgent, params.toAgent)); + } + if (params.thread) { + conditions.push(eq(schema.agentMessages.thread, params.thread)); + } + if (params.channel) { + conditions.push(eq(schema.agentMessages.channel, params.channel)); + } + if (params.sinceTs) { + conditions.push(sql`${schema.agentMessages.messageTs} >= ${params.sinceTs}`); + } + if (params.beforeTs) { + conditions.push(sql`${schema.agentMessages.messageTs} < ${params.beforeTs}`); + } + if (!params.includeExpired) { + conditions.push( + sql`(${schema.agentMessages.expiresAt} IS NULL OR ${schema.agentMessages.expiresAt} > NOW())` + ); + } + + let query = db + .select() + .from(schema.agentMessages) + .where(and(...conditions)) + .orderBy(desc(schema.agentMessages.messageTs)); + + if (params.limit) { + query = query.limit(params.limit) as typeof query; + } + if (params.offset) { + query = query.offset(params.offset) as typeof query; + } + + return query; + }, + + async getUnindexed(workspaceId: string, limit = 100): Promise { + const db = getDb(); + return db + .select() + .from(schema.agentMessages) + .where( + and( + eq(schema.agentMessages.workspaceId, workspaceId), + isNull(schema.agentMessages.indexedAt), + sql`(${schema.agentMessages.expiresAt} IS NULL OR ${schema.agentMessages.expiresAt} > NOW())` + ) + ) + .orderBy(schema.agentMessages.messageTs) + .limit(limit); + }, + + async markIndexed(ids: string[]): Promise { + if (ids.length === 0) return; + const db = getDb(); + await db + .update(schema.agentMessages) + .set({ indexedAt: new Date() }) + .where(inArray(schema.agentMessages.id, ids)); + }, + + async deleteExpired(): Promise { + const db = getDb(); + const result = await db + .delete(schema.agentMessages) + .where( + and( + isNotNull(schema.agentMessages.expiresAt), + lt(schema.agentMessages.expiresAt, new Date()) + ) + ) + .returning({ id: schema.agentMessages.id }); + return result.length; + }, + + async countByWorkspace(workspaceId: string): Promise { + const db = getDb(); + const result = await db + .select({ count: sql`count(*)` }) + .from(schema.agentMessages) + .where(eq(schema.agentMessages.workspaceId, workspaceId)); + return Number(result[0]?.count ?? 0); + }, + + async getThreadMessages(workspaceId: string, thread: string, limit = 50): Promise { + const db = getDb(); + return db + .select() + .from(schema.agentMessages) + .where( + and( + eq(schema.agentMessages.workspaceId, workspaceId), + eq(schema.agentMessages.thread, thread), + sql`(${schema.agentMessages.expiresAt} IS NULL OR ${schema.agentMessages.expiresAt} > NOW())` + ) + ) + .orderBy(schema.agentMessages.messageTs) + .limit(limit); + }, +}; + // ============================================================================ // Migration helper // ============================================================================ diff --git a/src/cloud/db/index.ts b/src/cloud/db/index.ts index f83e8951..55009d0a 100644 --- a/src/cloud/db/index.ts +++ b/src/cloud/db/index.ts @@ -49,6 +49,10 @@ export type { CommentMention, NewCommentMention, AgentTriggerConfig, + // Agent message types + AgentMessage, + NewAgentMessage, + MessagePayloadMeta, } from './schema.js'; // Re-export schema tables for direct access if needed @@ -67,6 +71,7 @@ export { ciFixAttempts as ciFixAttemptsTable, issueAssignments as issueAssignmentsTable, commentMentions as commentMentionsTable, + agentMessages as agentMessagesTable, } from './schema.js'; // Import query modules @@ -86,6 +91,7 @@ import { ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, + agentMessageQueries, } from './drizzle.js'; // Legacy type aliases for backwards compatibility @@ -116,6 +122,8 @@ export const db = { // Issue and comment tracking issueAssignments: issueAssignmentQueries, commentMentions: commentMentionQueries, + // Agent messages (cloud-synced relay messages) + agentMessages: agentMessageQueries, // Database utilities getDb, close: closeDb, @@ -136,6 +144,7 @@ export { ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, + agentMessageQueries, }; // Export database utilities diff --git a/src/cloud/db/migrations/0012_agent_messages.sql b/src/cloud/db/migrations/0012_agent_messages.sql new file mode 100644 index 00000000..b316bddb --- /dev/null +++ b/src/cloud/db/migrations/0012_agent_messages.sql @@ -0,0 +1,47 @@ +-- Agent Messages table for cloud-synced message history +-- Stores relay messages from daemons for search and retention + +CREATE TABLE "agent_messages" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "workspace_id" uuid NOT NULL, + "daemon_id" uuid, + "original_id" varchar(255) NOT NULL, + "from_agent" varchar(255) NOT NULL, + "to_agent" varchar(255) NOT NULL, + "body" text NOT NULL, + "kind" varchar(50) DEFAULT 'message' NOT NULL, + "topic" varchar(255), + "thread" varchar(255), + "channel" varchar(255), + "is_broadcast" boolean DEFAULT false NOT NULL, + "is_urgent" boolean DEFAULT false NOT NULL, + "data" jsonb, + "payload_meta" jsonb, + "message_ts" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "expires_at" timestamp, + "indexed_at" timestamp, + CONSTRAINT "agent_messages_workspace_original_unique" UNIQUE("workspace_id","original_id") +); +--> statement-breakpoint +ALTER TABLE "agent_messages" ADD CONSTRAINT "agent_messages_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "agent_messages" ADD CONSTRAINT "agent_messages_daemon_id_linked_daemons_id_fk" FOREIGN KEY ("daemon_id") REFERENCES "public"."linked_daemons"("id") ON DELETE set null ON UPDATE no action; +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_workspace_id" ON "agent_messages" USING btree ("workspace_id"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_daemon_id" ON "agent_messages" USING btree ("daemon_id"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_from_agent" ON "agent_messages" USING btree ("from_agent"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_to_agent" ON "agent_messages" USING btree ("to_agent"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_thread" ON "agent_messages" USING btree ("thread"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_channel" ON "agent_messages" USING btree ("channel"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_message_ts" ON "agent_messages" USING btree ("message_ts"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_expires_at" ON "agent_messages" USING btree ("expires_at"); +--> statement-breakpoint +CREATE INDEX "idx_agent_messages_indexed_at" ON "agent_messages" USING btree ("indexed_at"); diff --git a/src/cloud/db/migrations/meta/0011_snapshot.json b/src/cloud/db/migrations/meta/0011_snapshot.json new file mode 100644 index 00000000..88c248ce --- /dev/null +++ b/src/cloud/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,3273 @@ +{ + "id": "1462a768-d863-4ab8-aaa9-9cf3ed289755", + "prevId": "e7203b21-9353-4267-84cd-1317dba0ec55", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_crashes": { + "name": "agent_crashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "daemon_id": { + "name": "daemon_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likely_cause": { + "name": "likely_cause", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "peak_memory": { + "name": "peak_memory", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_known_memory": { + "name": "last_known_memory", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_trend": { + "name": "memory_trend", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "insight_data": { + "name": "insight_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_output": { + "name": "last_output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crashed_at": { + "name": "crashed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_crashes_daemon_id": { + "name": "idx_agent_crashes_daemon_id", + "columns": [ + { + "expression": "daemon_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_crashes_agent_name": { + "name": "idx_agent_crashes_agent_name", + "columns": [ + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_crashes_crashed_at": { + "name": "idx_agent_crashes_crashed_at", + "columns": [ + { + "expression": "crashed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_crashes_likely_cause": { + "name": "idx_agent_crashes_likely_cause", + "columns": [ + { + "expression": "likely_cause", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_crashes_daemon_id_linked_daemons_id_fk": { + "name": "agent_crashes_daemon_id_linked_daemons_id_fk", + "tableFrom": "agent_crashes", + "tableTo": "linked_daemons", + "columnsFrom": [ + "daemon_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_messages": { + "name": "agent_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "daemon_id": { + "name": "daemon_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_id": { + "name": "original_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "from_agent": { + "name": "from_agent", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "to_agent": { + "name": "to_agent", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'message'" + }, + "topic": { + "name": "topic", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "thread": { + "name": "thread", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_broadcast": { + "name": "is_broadcast", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_urgent": { + "name": "is_urgent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload_meta": { + "name": "payload_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "message_ts": { + "name": "message_ts", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_messages_workspace_id": { + "name": "idx_agent_messages_workspace_id", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_daemon_id": { + "name": "idx_agent_messages_daemon_id", + "columns": [ + { + "expression": "daemon_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_from_agent": { + "name": "idx_agent_messages_from_agent", + "columns": [ + { + "expression": "from_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_to_agent": { + "name": "idx_agent_messages_to_agent", + "columns": [ + { + "expression": "to_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_thread": { + "name": "idx_agent_messages_thread", + "columns": [ + { + "expression": "thread", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_channel": { + "name": "idx_agent_messages_channel", + "columns": [ + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_message_ts": { + "name": "idx_agent_messages_message_ts", + "columns": [ + { + "expression": "message_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_expires_at": { + "name": "idx_agent_messages_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_messages_indexed_at": { + "name": "idx_agent_messages_indexed_at", + "columns": [ + { + "expression": "indexed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_messages_workspace_id_workspaces_id_fk": { + "name": "agent_messages_workspace_id_workspaces_id_fk", + "tableFrom": "agent_messages", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_messages_daemon_id_linked_daemons_id_fk": { + "name": "agent_messages_daemon_id_linked_daemons_id_fk", + "tableFrom": "agent_messages", + "tableTo": "linked_daemons", + "columnsFrom": [ + "daemon_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_messages_workspace_original_unique": { + "name": "agent_messages_workspace_original_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "original_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_metrics": { + "name": "agent_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "daemon_id": { + "name": "daemon_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "rss_bytes": { + "name": "rss_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "heap_used_bytes": { + "name": "heap_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "trend": { + "name": "trend", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "trend_rate_per_minute": { + "name": "trend_rate_per_minute", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "alert_level": { + "name": "alert_level", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'normal'" + }, + "high_watermark": { + "name": "high_watermark", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "average_rss": { + "name": "average_rss", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "metrics_data": { + "name": "metrics_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "uptime_ms": { + "name": "uptime_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_metrics_daemon_id": { + "name": "idx_agent_metrics_daemon_id", + "columns": [ + { + "expression": "daemon_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_metrics_agent_name": { + "name": "idx_agent_metrics_agent_name", + "columns": [ + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_metrics_recorded_at": { + "name": "idx_agent_metrics_recorded_at", + "columns": [ + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_metrics_alert_level": { + "name": "idx_agent_metrics_alert_level", + "columns": [ + { + "expression": "alert_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_metrics_daemon_id_linked_daemons_id_fk": { + "name": "agent_metrics_daemon_id_linked_daemons_id_fk", + "tableFrom": "agent_metrics", + "tableTo": "linked_daemons", + "columnsFrom": [ + "daemon_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_sessions": { + "name": "agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_marker": { + "name": "end_marker", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_agent_sessions_workspace_id": { + "name": "idx_agent_sessions_workspace_id", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_sessions_agent_name": { + "name": "idx_agent_sessions_agent_name", + "columns": [ + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_sessions_status": { + "name": "idx_agent_sessions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_sessions_workspace_id_workspaces_id_fk": { + "name": "agent_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "agent_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_summaries": { + "name": "agent_summaries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_summaries_session_id": { + "name": "idx_agent_summaries_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_summaries_agent_name": { + "name": "idx_agent_summaries_agent_name", + "columns": [ + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_summaries_session_id_agent_sessions_id_fk": { + "name": "agent_summaries_session_id_agent_sessions_id_fk", + "tableFrom": "agent_summaries", + "tableTo": "agent_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ci_failure_events": { + "name": "ci_failure_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "check_name": { + "name": "check_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "conclusion": { + "name": "conclusion", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "failure_title": { + "name": "failure_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_summary": { + "name": "failure_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_details": { + "name": "failure_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "workflow_name": { + "name": "workflow_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "agent_spawned": { + "name": "agent_spawned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ci_failure_events_repository": { + "name": "idx_ci_failure_events_repository", + "columns": [ + { + "expression": "repository", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_failure_events_pr_number": { + "name": "idx_ci_failure_events_pr_number", + "columns": [ + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_failure_events_check_name": { + "name": "idx_ci_failure_events_check_name", + "columns": [ + { + "expression": "check_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_failure_events_created_at": { + "name": "idx_ci_failure_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_failure_events_repo_pr": { + "name": "idx_ci_failure_events_repo_pr", + "columns": [ + { + "expression": "repository", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ci_failure_events_repository_id_repositories_id_fk": { + "name": "ci_failure_events_repository_id_repositories_id_fk", + "tableFrom": "ci_failure_events", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ci_fix_attempts": { + "name": "ci_fix_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "failure_event_id": { + "name": "failure_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ci_fix_attempts_failure_event": { + "name": "idx_ci_fix_attempts_failure_event", + "columns": [ + { + "expression": "failure_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_fix_attempts_status": { + "name": "idx_ci_fix_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ci_fix_attempts_agent_id": { + "name": "idx_ci_fix_attempts_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ci_fix_attempts_failure_event_id_ci_failure_events_id_fk": { + "name": "ci_fix_attempts_failure_event_id_ci_failure_events_id_fk", + "tableFrom": "ci_fix_attempts", + "tableTo": "ci_failure_events", + "columnsFrom": [ + "failure_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_mentions": { + "name": "comment_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "issue_or_pr_number": { + "name": "issue_or_pr_number", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "comment_body": { + "name": "comment_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comment_url": { + "name": "comment_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "mentioned_agent": { + "name": "mentioned_agent", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "mention_context": { + "name": "mention_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "response_comment_id": { + "name": "response_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_comment_mentions_repository": { + "name": "idx_comment_mentions_repository", + "columns": [ + { + "expression": "repository", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comment_mentions_source": { + "name": "idx_comment_mentions_source", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comment_mentions_status": { + "name": "idx_comment_mentions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comment_mentions_mentioned_agent": { + "name": "idx_comment_mentions_mentioned_agent", + "columns": [ + { + "expression": "mentioned_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_mentions_repository_id_repositories_id_fk": { + "name": "comment_mentions_repository_id_repositories_id_fk", + "tableFrom": "comment_mentions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_account_email": { + "name": "provider_account_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credentials_user_id": { + "name": "idx_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credentials_user_provider_unique": { + "name": "credentials_user_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "installation_id": { + "name": "installation_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "installed_by_id": { + "name": "installed_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "suspended": { + "name": "suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_installations_account_login": { + "name": "idx_github_installations_account_login", + "columns": [ + { + "expression": "account_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_installations_installed_by": { + "name": "idx_github_installations_installed_by", + "columns": [ + { + "expression": "installed_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_installed_by_id_users_id_fk": { + "name": "github_installations_installed_by_id_users_id_fk", + "tableFrom": "github_installations", + "tableTo": "users", + "columnsFrom": [ + "installed_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_installations_installation_id_unique": { + "name": "github_installations_installation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "installation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_assignments": { + "name": "issue_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_url": { + "name": "issue_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_pr_number": { + "name": "linked_pr_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issue_assignments_repository": { + "name": "idx_issue_assignments_repository", + "columns": [ + { + "expression": "repository", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_assignments_issue_number": { + "name": "idx_issue_assignments_issue_number", + "columns": [ + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_assignments_status": { + "name": "idx_issue_assignments_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_assignments_agent_id": { + "name": "idx_issue_assignments_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_assignments_repository_id_repositories_id_fk": { + "name": "issue_assignments_repository_id_repositories_id_fk", + "tableFrom": "issue_assignments", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "issue_assignments_repo_issue_unique": { + "name": "issue_assignments_repo_issue_unique", + "nullsNotDistinct": false, + "columns": [ + "repository", + "issue_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.linked_daemons": { + "name": "linked_daemons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "api_key_hash": { + "name": "api_key_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "pending_updates": { + "name": "pending_updates", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "message_queue": { + "name": "message_queue", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_linked_daemons_user_id": { + "name": "idx_linked_daemons_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_linked_daemons_workspace_id": { + "name": "idx_linked_daemons_workspace_id", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_linked_daemons_api_key_hash": { + "name": "idx_linked_daemons_api_key_hash", + "columns": [ + { + "expression": "api_key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_linked_daemons_status": { + "name": "idx_linked_daemons_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "linked_daemons_user_id_users_id_fk": { + "name": "linked_daemons_user_id_users_id_fk", + "tableFrom": "linked_daemons", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "linked_daemons_workspace_id_workspaces_id_fk": { + "name": "linked_daemons_workspace_id_workspaces_id_fk", + "tableFrom": "linked_daemons", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "linked_daemons_user_machine_unique": { + "name": "linked_daemons_user_machine_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "machine_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_alerts": { + "name": "memory_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "daemon_id": { + "name": "daemon_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_type": { + "name": "alert_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "current_rss": { + "name": "current_rss", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "threshold": { + "name": "threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_memory_alerts_daemon_id": { + "name": "idx_memory_alerts_daemon_id", + "columns": [ + { + "expression": "daemon_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_memory_alerts_agent_name": { + "name": "idx_memory_alerts_agent_name", + "columns": [ + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_memory_alerts_alert_type": { + "name": "idx_memory_alerts_alert_type", + "columns": [ + { + "expression": "alert_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_memory_alerts_created_at": { + "name": "idx_memory_alerts_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_alerts_daemon_id_linked_daemons_id_fk": { + "name": "memory_alerts_daemon_id_linked_daemons_id_fk", + "tableFrom": "memory_alerts", + "tableTo": "linked_daemons", + "columnsFrom": [ + "daemon_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_groups": { + "name": "project_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "coordinator_agent": { + "name": "coordinator_agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"enabled\":false}'::jsonb" + }, + "sort_order": { + "name": "sort_order", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_groups_user_id": { + "name": "idx_project_groups_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_groups_user_id_users_id_fk": { + "name": "project_groups_user_id_users_id_fk", + "tableFrom": "project_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_groups_user_name_unique": { + "name": "project_groups_user_name_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_group_id": { + "name": "project_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "nango_connection_id": { + "name": "nango_connection_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "github_full_name": { + "name": "github_full_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "github_id": { + "name": "github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_status": { + "name": "sync_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_agent": { + "name": "project_agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"enabled\":false}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_repositories_user_id": { + "name": "idx_repositories_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_repositories_workspace_id": { + "name": "idx_repositories_workspace_id", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_repositories_project_group_id": { + "name": "idx_repositories_project_group_id", + "columns": [ + { + "expression": "project_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_repositories_installation_id": { + "name": "idx_repositories_installation_id", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_repositories_nango_connection": { + "name": "idx_repositories_nango_connection", + "columns": [ + { + "expression": "nango_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repositories_user_id_users_id_fk": { + "name": "repositories_user_id_users_id_fk", + "tableFrom": "repositories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "repositories_workspace_id_workspaces_id_fk": { + "name": "repositories_workspace_id_workspaces_id_fk", + "tableFrom": "repositories", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repositories_project_group_id_project_groups_id_fk": { + "name": "repositories_project_group_id_project_groups_id_fk", + "tableFrom": "repositories", + "tableTo": "project_groups", + "columnsFrom": [ + "project_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repositories_installation_id_github_installations_id_fk": { + "name": "repositories_installation_id_github_installations_id_fk", + "tableFrom": "repositories", + "tableTo": "github_installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_user_github_unique": { + "name": "repositories_user_github_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "github_full_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_stripe_subscription_id_unique": { + "name": "subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_records": { + "name": "usage_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metric": { + "name": "metric", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_usage_records_user_id": { + "name": "idx_usage_records_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_records_recorded_at": { + "name": "idx_usage_records_recorded_at", + "columns": [ + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_records_user_id_users_id_fk": { + "name": "usage_records_user_id_users_id_fk", + "tableFrom": "usage_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_records_workspace_id_workspaces_id_fk": { + "name": "usage_records_workspace_id_workspaces_id_fk", + "tableFrom": "usage_records", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_id": { + "name": "github_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "github_username": { + "name": "github_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "nango_connection_id": { + "name": "nango_connection_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "incoming_connection_id": { + "name": "incoming_connection_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "pending_installation_request": { + "name": "pending_installation_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_users_nango_connection": { + "name": "idx_users_nango_connection", + "columns": [ + { + "expression": "nango_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_incoming_connection": { + "name": "idx_users_incoming_connection", + "columns": [ + { + "expression": "incoming_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_github_id_unique": { + "name": "users_github_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_workspace_members_workspace_id": { + "name": "idx_workspace_members_workspace_id", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_members_user_id": { + "name": "idx_workspace_members_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_members_workspace_id_workspaces_id_fk": { + "name": "workspace_members_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_user_id_users_id_fk": { + "name": "workspace_members_user_id_users_id_fk", + "tableFrom": "workspace_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_invited_by_users_id_fk": { + "name": "workspace_members_invited_by_users_id_fk", + "tableFrom": "workspace_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_workspace_user_unique": { + "name": "workspace_members_workspace_user_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "compute_provider": { + "name": "compute_provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "compute_id": { + "name": "compute_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "public_url": { + "name": "public_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain_status": { + "name": "custom_domain_status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspaces_user_id": { + "name": "idx_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_custom_domain": { + "name": "idx_workspaces_custom_domain", + "columns": [ + { + "expression": "custom_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_user_id_users_id_fk": { + "name": "workspaces_user_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/cloud/db/migrations/meta/_journal.json b/src/cloud/db/migrations/meta/_journal.json index 9c1d063b..9dfe503f 100644 --- a/src/cloud/db/migrations/meta/_journal.json +++ b/src/cloud/db/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1736294400000, "tag": "0011_linked_daemon_workspace", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1767915620397, + "tag": "0012_agent_messages", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/cloud/db/schema.ts b/src/cloud/db/schema.ts index a5b919fe..d8c7f94f 100644 --- a/src/cloud/db/schema.ts +++ b/src/cloud/db/schema.ts @@ -191,6 +191,7 @@ export const workspacesRelations = relations(workspaces, ({ one, many }) => ({ members: many(workspaceMembers), repositories: many(repositories), linkedDaemons: many(linkedDaemons), + messages: many(agentMessages), })); // ============================================================================ @@ -343,7 +344,7 @@ export const linkedDaemons = pgTable('linked_daemons', { statusIdx: index('idx_linked_daemons_status').on(table.status), })); -export const linkedDaemonsRelations = relations(linkedDaemons, ({ one }) => ({ +export const linkedDaemonsRelations = relations(linkedDaemons, ({ one, many }) => ({ user: one(users, { fields: [linkedDaemons.userId], references: [users.id], @@ -352,6 +353,7 @@ export const linkedDaemonsRelations = relations(linkedDaemons, ({ one }) => ({ fields: [linkedDaemons.workspaceId], references: [workspaces.id], }), + messages: many(agentMessages), })); // ============================================================================ @@ -820,3 +822,100 @@ export type IssueAssignment = typeof issueAssignments.$inferSelect; export type NewIssueAssignment = typeof issueAssignments.$inferInsert; export type CommentMention = typeof commentMentions.$inferSelect; export type NewCommentMention = typeof commentMentions.$inferInsert; + +// ============================================================================ +// Agent Messages (cloud-synced message history) +// ============================================================================ + +/** + * Message payload metadata (mirrors SendMeta from protocol) + */ +export interface MessagePayloadMeta { + requires_ack?: boolean; + ttl_ms?: number; + importance?: number; // 0-100, 100 is highest + replyTo?: string; // Correlation ID for replies +} + +/** + * Agent messages table - stores all relay messages for search and history. + * + * Retention policy: + * - Free tier: 30 days (enforced via expires_at) + * - Pro tier: 90 days + * - Enterprise: Unlimited (no expires_at set) + * + * Messages are synced from daemon SQLite to cloud PostgreSQL. + */ +export const agentMessages = pgTable('agent_messages', { + id: uuid('id').primaryKey().defaultRandom(), + + // Scoping + workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + daemonId: uuid('daemon_id').references(() => linkedDaemons.id, { onDelete: 'set null' }), + + // Original message ID from daemon (for deduplication) + originalId: varchar('original_id', { length: 255 }).notNull(), + + // Core message fields (aligned with StoredMessage) + fromAgent: varchar('from_agent', { length: 255 }).notNull(), + toAgent: varchar('to_agent', { length: 255 }).notNull(), // '*' for broadcast + body: text('body').notNull(), + + // Message classification + kind: varchar('kind', { length: 50 }).notNull().default('message'), // message, action, state, thinking + topic: varchar('topic', { length: 255 }), + thread: varchar('thread', { length: 255 }), // Thread ID for grouping + channel: varchar('channel', { length: 255 }), // Channel name if channel message + + // Flags + isBroadcast: boolean('is_broadcast').notNull().default(false), + isUrgent: boolean('is_urgent').notNull().default(false), + + // Optional structured data + data: jsonb('data').$type>(), + payloadMeta: jsonb('payload_meta').$type(), + + // Timestamps + messageTs: timestamp('message_ts').notNull(), // Original message timestamp from daemon + createdAt: timestamp('created_at').defaultNow().notNull(), // When synced to cloud + expiresAt: timestamp('expires_at'), // Retention policy - null = never expires + + // Search/indexing tracking + indexedAt: timestamp('indexed_at'), // When sent to Algolia (null = not indexed) +}, (table) => ({ + // Primary lookups + workspaceIdIdx: index('idx_agent_messages_workspace_id').on(table.workspaceId), + daemonIdIdx: index('idx_agent_messages_daemon_id').on(table.daemonId), + + // Deduplication - prevent duplicate sync + workspaceOriginalIdx: unique('agent_messages_workspace_original_unique').on(table.workspaceId, table.originalId), + + // Query patterns + fromAgentIdx: index('idx_agent_messages_from_agent').on(table.fromAgent), + toAgentIdx: index('idx_agent_messages_to_agent').on(table.toAgent), + threadIdx: index('idx_agent_messages_thread').on(table.thread), + channelIdx: index('idx_agent_messages_channel').on(table.channel), + messageTsIdx: index('idx_agent_messages_message_ts').on(table.messageTs), + + // Retention cleanup + expiresAtIdx: index('idx_agent_messages_expires_at').on(table.expiresAt), + + // Search indexing queue + indexedAtIdx: index('idx_agent_messages_indexed_at').on(table.indexedAt), +})); + +export const agentMessagesRelations = relations(agentMessages, ({ one }) => ({ + workspace: one(workspaces, { + fields: [agentMessages.workspaceId], + references: [workspaces.id], + }), + daemon: one(linkedDaemons, { + fields: [agentMessages.daemonId], + references: [linkedDaemons.id], + }), +})); + +// Type exports for messages +export type AgentMessage = typeof agentMessages.$inferSelect; +export type NewAgentMessage = typeof agentMessages.$inferInsert; diff --git a/src/daemon/cloud-sync.ts b/src/daemon/cloud-sync.ts index 3f85295c..9330bf66 100644 --- a/src/daemon/cloud-sync.ts +++ b/src/daemon/cloud-sync.ts @@ -14,6 +14,7 @@ import * as path from 'path'; import * as os from 'os'; import { randomBytes } from 'crypto'; import { createLogger } from '../utils/logger.js'; +import type { StorageAdapter, StoredMessage } from '../storage/adapter.js'; const log = createLogger('cloud-sync'); @@ -22,6 +23,10 @@ export interface CloudSyncConfig { cloudUrl: string; heartbeatInterval: number; // ms enabled: boolean; + /** Enable message sync to cloud (default: true if connected) */ + messageSyncEnabled?: boolean; + /** Batch size for message sync (default: 100) */ + messageSyncBatchSize?: number; } export interface RemoteAgent { @@ -51,6 +56,9 @@ export class CloudSyncService extends EventEmitter { private localAgents: Map = new Map(); private remoteAgents: RemoteAgent[] = []; private connected = false; + private storage: StorageAdapter | null = null; + private lastMessageSyncTs: number = 0; + private messageSyncInProgress = false; constructor(config: Partial = {}) { super(); @@ -232,10 +240,11 @@ export class CloudSyncService extends EventEmitter { } } - // Fetch messages and sync agents + // Fetch messages, sync agents, and sync local messages to cloud await Promise.all([ this.fetchMessages(), this.syncAgents(), + this.syncMessagesToCloud(), ]); } catch (error) { log.error('Heartbeat error', { error: String(error) }); @@ -346,6 +355,97 @@ export class CloudSyncService extends EventEmitter { getMachineIdentifier(): string { return this.machineId; } + + /** + * Set the storage adapter for message sync + */ + setStorage(storage: StorageAdapter): void { + this.storage = storage; + log.info('Storage adapter configured for message sync'); + } + + /** + * Sync local messages to cloud storage + * + * Reads messages from local SQLite since last sync and posts them + * to the cloud API for centralized storage and search. + */ + async syncMessagesToCloud(): Promise<{ synced: number; duplicates: number }> { + // Skip if disabled, not connected, no storage, or sync in progress + if (!this.connected || !this.storage || this.messageSyncInProgress) { + return { synced: 0, duplicates: 0 }; + } + + if (this.config.messageSyncEnabled === false) { + return { synced: 0, duplicates: 0 }; + } + + this.messageSyncInProgress = true; + + try { + const batchSize = this.config.messageSyncBatchSize || 100; + + // Get messages since last sync + const messages = await this.storage.getMessages({ + sinceTs: this.lastMessageSyncTs > 0 ? this.lastMessageSyncTs : undefined, + limit: batchSize, + order: 'asc', + }); + + if (messages.length === 0) { + return { synced: 0, duplicates: 0 }; + } + + // Transform to API format + const syncPayload = messages.map((msg: StoredMessage) => ({ + id: msg.id, + ts: msg.ts, + from: msg.from, + to: msg.to, + body: msg.body, + kind: msg.kind, + topic: msg.topic, + thread: msg.thread, + is_broadcast: msg.is_broadcast, + is_urgent: msg.is_urgent, + data: msg.data, + payload_meta: msg.payloadMeta, + })); + + // Post to cloud + const response = await fetch(`${this.config.cloudUrl}/api/daemons/messages/sync`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messages: syncPayload }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Message sync failed: ${response.status} - ${errorText}`); + } + + const result = await response.json() as { synced: number; duplicates: number }; + + // Update last sync timestamp to the newest message we synced + if (messages.length > 0) { + this.lastMessageSyncTs = Math.max(...messages.map((m: StoredMessage) => m.ts)); + } + + if (result.synced > 0) { + log.info(`Synced ${result.synced} messages to cloud`, { duplicates: result.duplicates }); + } + + return result; + } catch (error) { + log.error('Message sync error', { error: String(error) }); + return { synced: 0, duplicates: 0 }; + } finally { + this.messageSyncInProgress = false; + } + } } // Singleton instance diff --git a/src/daemon/server.ts b/src/daemon/server.ts index 56e94d61..24cbaa7e 100644 --- a/src/daemon/server.ts +++ b/src/daemon/server.ts @@ -231,6 +231,12 @@ export class Daemon { }); await this.cloudSync.start(); + + // Set storage adapter for message sync to cloud + if (this.storage) { + this.cloudSync.setStorage(this.storage); + } + log.info('Cloud sync enabled'); } catch (err) { log.error('Failed to initialize cloud sync', { error: String(err) });