Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>

This commit is contained in:
OpenClaw Bot 2026-02-25 15:50:06 -06:00
parent 59f85de572
commit 62b730dae1
12 changed files with 396 additions and 45 deletions

View File

@ -0,0 +1,92 @@
/**
* Voxyz API Routes - Cap Gates
* Handles cap gate management and capacity checks
*/
import { NextRequest, NextResponse } from 'next/server';
import {
getActiveCapGates,
getCapacityStatus,
canAddProposal,
initializeCapGates,
checkCapGates
} from '@/lib/services/cap-gates.service';
// GET /api/voxyz/cap-gates - Get cap gates status
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const status = searchParams.get('status');
const check = searchParams.get('check');
if (status === 'capacity') {
const capacity = await getCapacityStatus();
return NextResponse.json({ capacity });
}
if (check === 'proposal') {
const estimatedMinutes = parseInt(searchParams.get('minutes') || '120');
const canAdd = await canAddProposal(estimatedMinutes);
return NextResponse.json(canAdd);
}
const gates = await getActiveCapGates();
return NextResponse.json({ gates });
} catch (error) {
console.error('Error fetching cap gates:', error);
return NextResponse.json(
{ error: 'Failed to fetch cap gates' },
{ status: 500 }
);
}
}
// POST /api/voxyz/cap-gates - Initialize cap gates
export async function POST(request: NextRequest) {
try {
const success = await initializeCapGates();
if (!success) {
return NextResponse.json(
{ error: 'Failed to initialize cap gates' },
{ status: 500 }
);
}
return NextResponse.json({ success: true }, { status: 201 });
} catch (error) {
console.error('Error initializing cap gates:', error);
return NextResponse.json(
{ error: 'Failed to initialize cap gates' },
{ status: 500 }
);
}
}
// POST /api/voxyz/cap-gates/check - Check a proposal against cap gates
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { estimatedMinutes, existingProposals } = body;
if (!estimatedMinutes) {
return NextResponse.json(
{ error: 'Missing required field: estimatedMinutes' },
{ status: 400 }
);
}
const result = await checkCapGates(estimatedMinutes, existingProposals || []);
return NextResponse.json(result);
} catch (error) {
console.error('Error checking cap gates:', error);
return NextResponse.json(
{ error: 'Failed to check cap gates' },
{ status: 500 }
);
}
}

View File

@ -13,7 +13,7 @@ import {
runDailyMissionGeneration
} from '@/lib/services/daily-mission-agent';
import { getProposalsForDate, approveProposal, rejectProposal } from '@/lib/services/proposal.service';
import { createClient } from '@/lib/supabase/server';
import { getServiceSupabase } from '@/lib/supabase/client';
// GET /api/voxyz/daily-missions - Get today's daily mission
export async function GET(request: NextRequest) {
@ -38,7 +38,7 @@ export async function GET(request: NextRequest) {
}
// Fetch full proposal details
const supabase = await createClient();
const supabase = getServiceSupabase();
const proposalIds = [
dailyMission.primary_mission_id,
dailyMission.secondary_mission_id,

View File

@ -12,7 +12,7 @@ import {
getTopProposalsForToday,
expireOldProposals
} from '@/lib/services/proposal.service';
import { createClient } from '@/lib/supabase/server';
import { getServiceSupabase } from '@/lib/supabase/client';
// GET /api/voxyz/proposals - Get proposals
export async function GET(request: NextRequest) {
@ -33,7 +33,7 @@ export async function GET(request: NextRequest) {
}
// Get all proposals with optional status filter
const supabase = await createClient();
const supabase = getServiceSupabase();
let query = supabase.from('ops_mission_proposals').select('*');
if (status) {
@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
);
}
const supabase = await createClient();
const supabase = getServiceSupabase();
const proposal = {
proposal_date: new Date().toISOString().split('T')[0],

View File

@ -0,0 +1,150 @@
/**
* Voxyz API Routes - Trigger Rules
* Handles trigger rule execution and management
*/
import { NextRequest, NextResponse } from 'next/server';
import {
getActiveTriggerRules,
evaluateAllTriggers,
triggerManualEvaluation,
runTriggerMonitor,
getRecentTriggerLogs
} from '@/lib/services/trigger-rules.service';
import { fetchAllTasks } from '@/lib/data/tasks';
import { getServiceSupabase } from '@/lib/supabase/client';
// GET /api/voxyz/trigger-rules - Get trigger rules and logs
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const logs = searchParams.get('logs');
const limit = parseInt(searchParams.get('limit') || '50');
if (logs) {
const triggerLogs = await getRecentTriggerLogs(limit);
return NextResponse.json({ logs: triggerLogs });
}
const rules = await getActiveTriggerRules();
return NextResponse.json({ rules });
} catch (error) {
console.error('Error fetching trigger rules:', error);
return NextResponse.json(
{ error: 'Failed to fetch trigger rules' },
{ status: 500 }
);
}
}
// POST /api/voxyz/trigger-rules - Run trigger evaluation or manual trigger
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, triggerType, taskIds } = body;
if (action === 'run-monitor') {
// Run the trigger monitor agent
await runTriggerMonitor();
return NextResponse.json({
success: true,
message: 'Trigger monitor executed'
});
}
if (action === 'manual-trigger') {
if (!triggerType) {
return NextResponse.json(
{ error: 'Missing required field: triggerType' },
{ status: 400 }
);
}
const results = await triggerManualEvaluation(triggerType, taskIds);
return NextResponse.json({
success: true,
triggered: results.length,
results,
});
}
if (action === 'evaluate-all') {
// Evaluate all triggers against current context
const tasks = await fetchAllTasks();
const context = {
currentTime: new Date(),
activeTasks: tasks.filter(t => t.status !== 'done' && t.status !== 'canceled'),
recentProposals: [],
todayMissions: [],
userAvailability: [],
};
const results = await evaluateAllTriggers(context);
return NextResponse.json({
success: true,
evaluated: results.length,
triggered: results.filter(r => r.triggered).length,
results,
});
}
return NextResponse.json(
{ error: 'Unknown action' },
{ status: 400 }
);
} catch (error) {
console.error('Error running trigger evaluation:', error);
return NextResponse.json(
{ error: 'Failed to run trigger evaluation' },
{ status: 500 }
);
}
}
// PATCH /api/voxyz/trigger-rules - Update a trigger rule
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { id, isActive, priority } = body;
if (!id) {
return NextResponse.json(
{ error: 'Missing required field: id' },
{ status: 400 }
);
}
const supabase = getServiceSupabase();
const updates: Record<string, unknown> = {
updated_at: new Date().toISOString(),
};
if (isActive !== undefined) updates.is_active = isActive;
if (priority !== undefined) updates.priority = priority;
const { data, error } = await supabase
.from('ops_trigger_rules')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) {
throw error;
}
return NextResponse.json({ rule: data });
} catch (error) {
console.error('Error updating trigger rule:', error);
return NextResponse.json(
{ error: 'Failed to update trigger rule' },
{ status: 500 }
);
}
}

View File

@ -13,7 +13,7 @@ import {
MissionProposal,
DailyMission
} from '@/lib/supabase/voxyz.types';
import { createClient } from '@/lib/supabase/client';
import { getServiceSupabase } from '@/lib/supabase/client';
const DEFAULT_CONFIG: CapGateConfig = {
dailyMissionLimit: 3,
@ -28,7 +28,7 @@ const DEFAULT_CONFIG: CapGateConfig = {
* Get all active cap gates from the database
*/
export async function getActiveCapGates(): Promise<CapGate[]> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from('ops_cap_gates')
@ -48,7 +48,7 @@ export async function getActiveCapGates(): Promise<CapGate[]> {
* Get a specific cap gate by type
*/
export async function getCapGateByType(gateType: CapGate['gate_type']): Promise<CapGate | null> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from('ops_cap_gates')
@ -72,7 +72,7 @@ export async function updateCapGateValue(
gateId: string,
currentValue: number
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_cap_gates')
@ -387,7 +387,7 @@ export async function getCapacityStatus(config: CapGateConfig = DEFAULT_CONFIG):
focusTimeAllocatedMinutes: number;
reactionTimeReservedMinutes: number;
}> {
const supabase = createClient();
const supabase = getServiceSupabase();
const today = new Date().toISOString().split('T')[0];
// Get today's proposals
@ -446,7 +446,7 @@ export async function canAddProposal(
* Create default cap gates in database
*/
export async function initializeCapGates(config: CapGateConfig = DEFAULT_CONFIG): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
// Check if cap gates already exist
const { count } = await supabase
@ -557,7 +557,7 @@ export async function initializeCapGates(config: CapGateConfig = DEFAULT_CONFIG)
* Reset cap gate values for a new day
*/
export async function resetCapGatesForNewDay(): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_cap_gates')

View File

@ -28,7 +28,7 @@ import {
initializeCapGates,
resetCapGatesForNewDay
} from '@/lib/services/cap-gates.service';
import { createClient } from '@/lib/supabase/client';
import { getServiceSupabase } from '@/lib/supabase/client';
const GENERATION_HOUR = 7;
const GENERATION_MINUTE = 0;
@ -279,7 +279,7 @@ async function canAddAnotherProposal(
async function createCalendarBlockForProposal(
proposal: MissionProposal
): Promise<CalendarBlock | null> {
const supabase = createClient();
const supabase = getServiceSupabase();
const startTime = proposal.suggested_start_time
? new Date(proposal.suggested_start_time)
@ -325,7 +325,7 @@ async function createDailyMissionRecord(
proposals: MissionProposal[],
calendarBlocks: CalendarBlock[]
): Promise<DailyMission> {
const supabase = createClient();
const supabase = getServiceSupabase();
const totalMinutes = proposals.reduce((sum, p) =>
sum + (p.estimated_duration_minutes || 120), 0
@ -396,11 +396,11 @@ async function createDailyMissionRecord(
/**
* Start execution log entry
*/
async function startExecutionLog(
export async function startExecutionLog(
agentName: 'proposal_generator' | 'cap_gate_validator' | 'calendar_scheduler' | 'trigger_monitor' | 'mission_optimizer' | 'stale_task_detector',
executionType: 'scheduled' | 'triggered' | 'manual' | 'reaction'
): Promise<string> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from('ops_agent_execution_log')
@ -429,7 +429,7 @@ async function startExecutionLog(
/**
* Complete execution log entry
*/
async function completeExecutionLog(
export async function completeExecutionLog(
logId: string,
result: {
status: 'success' | 'partial' | 'failed';
@ -441,7 +441,7 @@ async function completeExecutionLog(
errorMessage?: string;
}
): Promise<void> {
const supabase = createClient();
const supabase = getServiceSupabase();
await supabase
.from('ops_agent_execution_log')
@ -470,7 +470,7 @@ function isNewDay(): boolean {
* Get today's daily mission
*/
export async function getTodaysDailyMission(): Promise<DailyMission | null> {
const supabase = createClient();
const supabase = getServiceSupabase();
const today = new Date().toISOString().split('T')[0];
const { data, error } = await supabase
@ -537,7 +537,7 @@ export async function runDailyMissionGeneration(): Promise<DailyMissionGeneratio
export async function approveDailyMission(
missionId: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_daily_missions')
@ -560,7 +560,7 @@ export async function approveDailyMission(
export async function scheduleDailyMission(
missionId: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
// Get mission details
const { data: mission } = await supabase
@ -621,7 +621,7 @@ export async function completeDailyMission(
rating?: number,
notes?: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_daily_missions')

View File

@ -13,7 +13,7 @@ import {
ProposalStatus,
ProposalServiceConfig
} from '@/lib/supabase/voxyz.types';
import { createClient } from '@/lib/supabase/client';
import { getServiceSupabase } from '@/lib/supabase/client';
import { Task } from '@/lib/data/tasks';
import { Project } from '@/lib/data/projects';
@ -136,9 +136,10 @@ function calculateProjectImportanceScore(project: Project | undefined): number {
function calculateBlockedScore(task: Task, context: ProposalContext): number {
if (task.status === 'blocked') {
// Check how long it's been blocked
const blockedHours = task.comments?.reduce((hours, comment) => {
if (comment.text?.toLowerCase().includes('blocked')) {
const commentTime = new Date(comment.createdAt);
const blockedHours = task.comments?.reduce<number>((hours, comment) => {
const item = comment as { text?: unknown; createdAt?: unknown };
if (typeof item.text === 'string' && item.text.toLowerCase().includes('blocked') && typeof item.createdAt === 'string') {
const commentTime = new Date(item.createdAt);
const blockedHours = (context.currentTime.getTime() - commentTime.getTime()) / (1000 * 60 * 60);
return Math.max(hours, blockedHours);
}
@ -386,12 +387,20 @@ export function createProposalFromTask(
source_task_ids: [task.id],
source_project_ids: project ? [project.id] : [],
title: task.title,
description: task.description,
description: task.description ?? null,
rationale,
ai_analysis: aiAnalysis,
suggested_start_time: suggestedStartTime?.toISOString() || null,
suggested_end_time: suggestedEndTime?.toISOString() || null,
estimated_duration_minutes: estimatedDuration,
approved_at: null,
approved_by: null,
rejected_at: null,
rejection_reason: null,
calendar_event_id: null,
calendar_synced_at: null,
executed_at: null,
execution_notes: null,
expires_at: new Date(context.currentTime.getTime() + config.defaultProposalExpiryHours * 60 * 60 * 1000).toISOString(),
};
}
@ -575,7 +584,7 @@ export async function saveProposal(
proposal: MissionProposalInsert,
capGateResult: CapGateCheckResult
): Promise<MissionProposal | null> {
const supabase = createClient();
const supabase = getServiceSupabase();
const proposalWithCapGates: MissionProposalInsert = {
...proposal,
@ -601,7 +610,7 @@ export async function saveProposal(
* Get proposals for a specific date
*/
export async function getProposalsForDate(date: Date): Promise<MissionProposal[]> {
const supabase = createClient();
const supabase = getServiceSupabase();
const dateString = date.toISOString().split('T')[0];
const { data, error } = await supabase
@ -625,7 +634,7 @@ export async function approveProposal(
proposalId: string,
userId: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_mission_proposals')
@ -651,7 +660,7 @@ export async function rejectProposal(
proposalId: string,
reason: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_mission_proposals')
@ -677,7 +686,7 @@ export async function completeProposal(
proposalId: string,
notes?: string
): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { error } = await supabase
.from('ops_mission_proposals')
@ -718,7 +727,7 @@ export async function getTopProposalsForToday(limit: number = 3): Promise<Missio
* Expire old proposals
*/
export async function expireOldProposals(): Promise<number> {
const supabase = createClient();
const supabase = getServiceSupabase();
const now = new Date().toISOString();
const { data, error } = await supabase

View File

@ -13,14 +13,14 @@ import {
TriggerLog
} from '@/lib/supabase/voxyz.types';
import { Task } from '@/lib/data/tasks';
import { createClient } from '@/lib/supabase/client';
import { getServiceSupabase } from '@/lib/supabase/client';
import { startExecutionLog, completeExecutionLog } from '@/lib/services/daily-mission-agent';
/**
* Get all active trigger rules
*/
export async function getActiveTriggerRules(): Promise<TriggerRule[]> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from('ops_trigger_rules')
@ -224,7 +224,7 @@ function evaluateHighPriorityTrigger(
const triggeredTasks = context.activeTasks.filter(task => {
if (task.priority !== priorityFilter) return false;
const createdAt = new Date(task.created_at || task.updatedAt);
const createdAt = new Date(task.createdAt || task.updatedAt);
return createdAt > oneHourAgo;
});
@ -419,7 +419,7 @@ async function isOnCooldown(rule: TriggerRule): Promise<boolean> {
* Check if trigger has reached daily limit
*/
async function hasReachedDailyLimit(rule: TriggerRule): Promise<boolean> {
const supabase = createClient();
const supabase = getServiceSupabase();
const today = new Date().toISOString().split('T')[0];
const { count, error } = await supabase
@ -440,7 +440,7 @@ async function hasReachedDailyLimit(rule: TriggerRule): Promise<boolean> {
* Update trigger timestamp
*/
async function updateTriggerTimestamp(ruleId: string): Promise<void> {
const supabase = createClient();
const supabase = getServiceSupabase();
await supabase
.from('ops_trigger_rules')
@ -459,7 +459,7 @@ async function logTrigger(
result: TriggerEvaluationResult,
success: boolean
): Promise<void> {
const supabase = createClient();
const supabase = getServiceSupabase();
await supabase
.from('ops_trigger_log')
@ -527,7 +527,7 @@ export async function triggerManualEvaluation(
taskIds?: string[]
): Promise<TriggerEvaluationResult[]> {
// Fetch active trigger rules of this type
const supabase = createClient();
const supabase = getServiceSupabase();
const { data: rules } = await supabase
.from('ops_trigger_rules')
@ -570,7 +570,7 @@ export async function triggerManualEvaluation(
* Get recent trigger logs
*/
export async function getRecentTriggerLogs(limit: number = 50): Promise<TriggerLog[]> {
const supabase = createClient();
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from('ops_trigger_log')

View File

@ -0,0 +1,13 @@
/**
* Voxyz Autonomous Architecture - Services Index
* Phase 10: Mission Control Autonomous System
*/
// Database types
export * from '@/lib/supabase/voxyz.types';
// Services
export * from '../proposal.service';
export * from '../cap-gates.service';
export * from '../daily-mission-agent';
export * from '../trigger-rules.service';

View File

@ -151,6 +151,9 @@ export interface Database {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Json = any;
// Voxyz Phase 10: Import Voxyz database extensions
export type { VoxyzDatabase } from './voxyz.types';
// Extended types for activity feed
export interface TaskComment {
id: string;

View File

@ -4,6 +4,7 @@
*/
import { Database as BaseDatabase } from './database.types';
import type { Task } from '@/lib/data/tasks';
// ============================================
// ENUM TYPES
@ -184,7 +185,7 @@ export interface DailyMission {
focus_time_start: string | null;
focus_time_end: string | null;
focus_time_minutes: number | null;
calendar_blocks: CalendarBlock[];
calendar_blocks: DailyMissionCalendarBlock[];
planned_reactions: number;
actual_reactions: number;
reaction_ratio: number;
@ -194,6 +195,13 @@ export interface DailyMission {
user_feedback: Record<string, unknown>;
}
export interface DailyMissionCalendarBlock {
id: string;
start_time: string;
end_time: string;
block_type: BlockType;
}
export interface CalendarBlock {
id: string;
created_at: string;
@ -253,7 +261,7 @@ export interface AgentExecutionLog {
export interface CapGateViolation {
gate_name: string;
gate_type: GateType;
gate_type: GateType | 'prioritization';
current_value: number;
max_value: number;
violation_message: string;
@ -342,7 +350,7 @@ export interface MissionProposalInput {
export interface TriggerEvaluationContext {
currentTime: Date;
activeTasks: BaseDatabase['public']['Tables']['tasks']['Row'][];
activeTasks: Task[];
recentProposals: MissionProposal[];
todayMissions: DailyMission[];
userAvailability: CalendarBlock[];

76
scripts/voxyz-daily-cron.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/bash
# Voxyz Daily Mission Generation Cron Job
# Runs at 7:00 AM daily to generate mission proposals
# Phase 10: Autonomous Architecture
set -e
# Configuration
API_BASE_URL="${VOXYZ_API_URL:-http://localhost:3001/api/voxyz}"
API_KEY="${VOXYZ_API_KEY:-}"
LOG_FILE="${VOXYZ_LOG_FILE:-/tmp/voxyz-daily-missions.log}"
# Timestamp
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] Starting Voxyz daily mission generation..." >> "$LOG_FILE"
# Function to make API calls
make_api_call() {
local endpoint=$1
local method=$2
local data=$3
if [ -n "$API_KEY" ]; then
curl -s -X "$method" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d "$data" \
"${API_BASE_URL}${endpoint}"
else
curl -s -X "$method" \
-H "Content-Type: application/json" \
-d "$data" \
"${API_BASE_URL}${endpoint}"
fi
}
# Step 1: Check if missions already generated for today
echo "[$TIMESTAMP] Checking for existing daily missions..." >> "$LOG_FILE"
EXISTING=$(make_api_call "/daily-missions" "GET" "")
if echo "$EXISTING" | grep -q '"dailyMission":null'; then
echo "[$TIMESTAMP] No existing daily mission found. Generating..." >> "$LOG_FILE"
# Step 2: Generate daily missions
RESULT=$(make_api_call "/daily-missions" "POST" '{"targetMissionCount": 3, "includeReactions": true}')
if echo "$RESULT" | grep -q '"dailyMission"'; then
echo "[$TIMESTAMP] Daily missions generated successfully" >> "$LOG_FILE"
# Extract proposal count
PROPOSAL_COUNT=$(echo "$RESULT" | grep -o '"proposals":\[[^]]*\]' | tr ',' '\n' | wc -l)
echo "[$TIMESTAMP] Generated $PROPOSAL_COUNT proposals" >> "$LOG_FILE"
# Step 3: Run trigger monitor to check for urgent items
echo "[$TIMESTAMP] Running trigger monitor..." >> "$LOG_FILE"
TRIGGER_RESULT=$(make_api_call "/trigger-rules" "POST" '{"action": "run-monitor"}')
echo "[$TIMESTAMP] Trigger monitor completed" >> "$LOG_FILE"
# Step 4: Clean up expired proposals
echo "[$TIMESTAMP] Cleaning up expired proposals..." >> "$LOG_FILE"
EXPIRED_RESULT=$(make_api_call "/proposals" "DELETE" "")
EXPIRED_COUNT=$(echo "$EXPIRED_RESULT" | grep -o '"expiredCount":[0-9]*' | cut -d: -f2)
echo "[$TIMESTAMP] Expired $EXPIRED_COUNT old proposals" >> "$LOG_FILE"
echo "[$TIMESTAMP] Daily mission generation completed successfully" >> "$LOG_FILE"
exit 0
else
echo "[$TIMESTAMP] ERROR: Failed to generate daily missions" >> "$LOG_FILE"
echo "[$TIMESTAMP] Response: $RESULT" >> "$LOG_FILE"
exit 1
fi
else
echo "[$TIMESTAMP] Daily missions already exist for today. Skipping generation." >> "$LOG_FILE"
exit 0
fi