diff --git a/app/components/Agents/Main.tsx b/app/components/Agents/Main.tsx
index 0a06a62b..4c285665 100644
--- a/app/components/Agents/Main.tsx
+++ b/app/components/Agents/Main.tsx
@@ -82,6 +82,43 @@ export const AgentsMain: React.FC<{ isSidebarOpen?: boolean }> = ({
const userAddress = getAddress();
const hasSession = isAuthenticated || !!userAddress;
+ const loadEnabledAgents = useCallback(async () => {
+ const userAddress = getAddress();
+ if (!userAddress) return;
+
+ try {
+ const response = await fetch(
+ `/api/agents/status?walletAddress=${userAddress}`
+ );
+ if (response.ok) {
+ const data = await response.json();
+ setEnabledAgents(data.agents || []);
+ }
+ } catch (error) {
+ console.error('Failed to load enabled agents:', error);
+ }
+ }, [getAddress]);
+
+ const loadUserConfig = useCallback(async () => {
+ const userAddress = getAddress();
+ if (!userAddress) return;
+
+ try {
+ const response = await fetch('/api/agents/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ walletAddress: userAddress }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setUserConfig(data.config || {});
+ }
+ } catch (error) {
+ console.error('Failed to load user agent config:', error);
+ }
+ }, [getAddress]);
+
const loadUserAgentData = useCallback(async () => {
const walletAddr = getAddress();
if (!walletAddr) return;
@@ -98,9 +135,9 @@ export const AgentsMain: React.FC<{ isSidebarOpen?: boolean }> = ({
isClosable: true,
});
}
- }, [getAddress, toast]);
+ }, [getAddress, toast, loadEnabledAgents, loadUserConfig]);
- const loadAvailableAgents = async () => {
+ const loadAvailableAgents = useCallback(async () => {
try {
const response = await fetch('/api/agents/available');
if (response.ok) {
@@ -133,51 +170,14 @@ export const AgentsMain: React.FC<{ isSidebarOpen?: boolean }> = ({
} catch (error) {
console.error('Failed to load available agents:', error);
}
- };
-
- const loadEnabledAgents = async () => {
- const userAddress = getAddress();
- if (!userAddress) return;
-
- try {
- const response = await fetch(
- `/api/agents/status?walletAddress=${userAddress}`
- );
- if (response.ok) {
- const data = await response.json();
- setEnabledAgents(data.agents || []);
- }
- } catch (error) {
- console.error('Failed to load enabled agents:', error);
- }
- };
-
- const loadUserConfig = async () => {
- const userAddress = getAddress();
- if (!userAddress) return;
-
- try {
- const response = await fetch('/api/agents/config', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ walletAddress: userAddress }),
- });
-
- if (response.ok) {
- const data = await response.json();
- setUserConfig(data.config || {});
- }
- } catch (error) {
- console.error('Failed to load user agent config:', error);
- }
- };
+ }, [defaultsInitialized]);
// Load available agents immediately (no auth needed)
useEffect(() => {
loadAvailableAgents().finally(() => {
setGlobalLoading(false);
});
- }, []);
+ }, [loadAvailableAgents]);
// Load user-specific data when authenticated
useEffect(() => {
diff --git a/app/components/Auth/AutoAuth.tsx b/app/components/Auth/AutoAuth.tsx
index e9e06e5b..9af98adf 100644
--- a/app/components/Auth/AutoAuth.tsx
+++ b/app/components/Auth/AutoAuth.tsx
@@ -11,7 +11,7 @@ export const AutoAuth = () => {
if (isConnected && address && !isAuthenticated) {
authenticate();
}
- }, [isConnected, address, isAuthenticated]);
+ }, [isConnected, address, isAuthenticated, authenticate]);
// No UI is rendered
return null;
diff --git a/app/components/Chat/index.tsx b/app/components/Chat/index.tsx
index f915ffb9..bf244b71 100755
--- a/app/components/Chat/index.tsx
+++ b/app/components/Chat/index.tsx
@@ -1,6 +1,7 @@
import { ChatInput } from '@/components/ChatInput';
import PrefilledOptions from '@/components/ChatInput/PrefilledOptions';
import { JobsList } from '@/components/JobsList';
+import { JobSuggestions } from '@/components/JobSuggestions';
import { MessageList } from '@/components/MessageList';
import { StatsCarousel } from '@/components/StatsCarousel';
import { useChatContext } from '@/contexts/chat/useChatContext';
@@ -485,6 +486,11 @@ export const Chat: FC<{
+ {/* Mobile Job Suggestions */}
+
+
+
+
)}
+ {/* Job Suggestions */}
+
+
+
+
{/* Jobs List - directly under input/options */}
= ({ show }) => {
} else if (!show && isOpen) {
onClose();
}
- }, [show, isOpen]);
+ }, [show, isOpen, onClose, onOpen]);
diff --git a/app/components/JobSuggestions/index.tsx b/app/components/JobSuggestions/index.tsx
index 050dcf2d..8bec6f8f 100644
--- a/app/components/JobSuggestions/index.tsx
+++ b/app/components/JobSuggestions/index.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import {
Box,
VStack,
@@ -43,7 +43,7 @@ export const JobSuggestions: React.FC = ({
const { getAddress } = useWalletAddress();
const toast = useToast();
- const fetchSuggestions = async () => {
+ const fetchSuggestions = useCallback(async () => {
const walletAddress = getAddress();
if (!walletAddress) return;
@@ -63,7 +63,7 @@ export const JobSuggestions: React.FC = ({
if (response.ok) {
const data = await response.json();
setSuggestions(data.suggestions || []);
-
+
trackEvent('job.suggestions_loaded');
}
} catch (error) {
@@ -71,7 +71,7 @@ export const JobSuggestions: React.FC = ({
} finally {
setLoading(false);
}
- };
+ }, [getAddress, userContext]);
const createJobFromSuggestion = async (suggestion: JobSuggestion) => {
const walletAddress = getAddress();
@@ -148,7 +148,7 @@ export const JobSuggestions: React.FC = ({
if (isVisible && suggestions.length === 0) {
fetchSuggestions();
}
- }, [isVisible]);
+ }, [isVisible, fetchSuggestions, suggestions.length]);
if (!isVisible || suggestions.length === 0) {
return null;
diff --git a/app/components/MessageCounter/index.tsx b/app/components/MessageCounter/index.tsx
index 9a1d4ed5..b3206dbd 100644
--- a/app/components/MessageCounter/index.tsx
+++ b/app/components/MessageCounter/index.tsx
@@ -26,7 +26,6 @@ export const MessageCounter: FC = ({
textAlign = 'center',
}) => {
const [stats, setStats] = useState(null);
- const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
const fetchStats = useCallback(async () => {
try {
@@ -48,70 +47,18 @@ export const MessageCounter: FC = ({
return () => clearInterval(interval);
}, [fetchStats]);
- // Carousel animation logic
- useEffect(() => {
- const messages = stats?.carouselMessages || [
- 'has completed 0 jobs',
- 'is ready to assist you',
- 'handles automated tasks',
- 'saves you time',
- ];
-
- if (messages.length <= 1) return;
-
- const interval = setInterval(() => {
- setCurrentMessageIndex((prev) => (prev + 1) % messages.length);
- }, 4000); // Change every 4 seconds
-
- return () => clearInterval(interval);
- }, [stats]);
-
- const messages = stats?.carouselMessages || [
- 'has completed 0 jobs',
- 'is ready to assist you',
- 'handles automated tasks',
- 'saves you time',
- ];
+ // Single message display - no carousel needed
+ const message = stats?.carouselMessages?.[0] || 'has completed 0 total jobs to date';
return (
-
-
-
+
- {/* FreeAI is ALWAYS visible and fixed */}
+ {/* FreeAI branding */}
= ({
{' '}
- {/* Carousel container - inline-block to stay on same line */}
-
- {messages.map((msg, index) => (
-
- {msg}
-
- ))}
-
+ {/* Single message display */}
+
+ {message}
+
);
diff --git a/app/components/Settings/A2AManagement.tsx b/app/components/Settings/A2AManagement.tsx
index b89cff7a..62a56938 100644
--- a/app/components/Settings/A2AManagement.tsx
+++ b/app/components/Settings/A2AManagement.tsx
@@ -46,7 +46,7 @@ import {
Users,
XCircle,
} from 'lucide-react';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
interface A2AManagementProps {
@@ -116,14 +116,7 @@ export const A2AManagement: React.FC = ({ onSave }) => {
const [messageType, setMessageType] = useState<'text' | 'json'>('text');
const [sendingMessage, setSendingMessage] = useState(false);
- // Load data on mount
- useEffect(() => {
- if (isAuthenticated) {
- loadConnectedAgents();
- }
- }, [isAuthenticated]);
-
- const loadConnectedAgents = async () => {
+ const loadConnectedAgents = useCallback(async () => {
const userAddress = getAddress();
if (!userAddress) return;
@@ -146,7 +139,14 @@ export const A2AManagement: React.FC = ({ onSave }) => {
} finally {
setGlobalLoading(false);
}
- };
+ }, [getAddress, toast]);
+
+ // Load data on mount
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadConnectedAgents();
+ }
+ }, [isAuthenticated, loadConnectedAgents]);
const handleDiscoverAgents = async () => {
const userAddress = getAddress();
diff --git a/app/components/Settings/MCPConfiguration.tsx b/app/components/Settings/MCPConfiguration.tsx
index d80fbfa5..aa8d63f8 100644
--- a/app/components/Settings/MCPConfiguration.tsx
+++ b/app/components/Settings/MCPConfiguration.tsx
@@ -23,7 +23,7 @@ import {
Settings,
XCircle,
} from 'lucide-react';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
interface MCPConfigurationProps {
@@ -70,24 +70,10 @@ export const MCPConfiguration: React.FC = ({
if (isAuthenticated && userAddress) {
loadMCPData();
}
- }, [isAuthenticated, getAddress]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isAuthenticated]);
- const loadMCPData = async () => {
- setGlobalLoading(true);
- try {
- await Promise.all([
- loadAvailableServers(),
- loadEnabledServers(),
- loadUserCredentials(),
- ]);
- } catch (error) {
- console.error('Failed to load MCP data:', error);
- } finally {
- setGlobalLoading(false);
- }
- };
-
- const loadAvailableServers = async () => {
+ const loadAvailableServers = useCallback(async () => {
try {
const response = await fetch('/api/mcp/available');
if (response.ok) {
@@ -97,9 +83,9 @@ export const MCPConfiguration: React.FC = ({
} catch (error) {
console.error('Failed to load available MCP servers:', error);
}
- };
+ }, []);
- const loadEnabledServers = async () => {
+ const loadEnabledServers = useCallback(async () => {
const userAddress = getAddress();
if (!userAddress) return;
@@ -114,9 +100,9 @@ export const MCPConfiguration: React.FC = ({
} catch (error) {
console.error('Failed to load enabled MCP servers:', error);
}
- };
+ }, [getAddress]);
- const loadUserCredentials = async () => {
+ const loadUserCredentials = useCallback(async () => {
const userAddress = getAddress();
if (!userAddress) return;
@@ -134,7 +120,22 @@ export const MCPConfiguration: React.FC = ({
} catch (error) {
console.error('Failed to load user credentials:', error);
}
- };
+ }, [getAddress]);
+
+ const loadMCPData = useCallback(async () => {
+ setGlobalLoading(true);
+ try {
+ await Promise.all([
+ loadAvailableServers(),
+ loadEnabledServers(),
+ loadUserCredentials(),
+ ]);
+ } catch (error) {
+ console.error('Failed to load MCP data:', error);
+ } finally {
+ setGlobalLoading(false);
+ }
+ }, [loadAvailableServers, loadEnabledServers, loadUserCredentials]);
const handleToggleServer = async (serverName: string, enable: boolean) => {
const userAddress = getAddress();
diff --git a/app/migrations/017-backfill-parent-job-ids.sql b/app/migrations/017-backfill-parent-job-ids.sql
new file mode 100644
index 00000000..255d1a85
--- /dev/null
+++ b/app/migrations/017-backfill-parent-job-ids.sql
@@ -0,0 +1,67 @@
+-- Backfill parent_job_id for existing jobs to enable proper threading
+-- This migration is IDEMPOTENT - it can be run multiple times safely
+-- It only updates jobs where parent_job_id IS NULL
+
+-- Step 1: For each wallet_address + name combination, find the oldest scheduled job
+-- and set it as the parent for all non-scheduled jobs with the same name
+-- Using DISTINCT ON with created_at ordering since UUID doesn't support MIN()
+
+DO $$
+BEGIN
+ -- Step 1: Link non-scheduled jobs to their scheduled parent (if exists)
+ WITH parent_jobs AS (
+ SELECT DISTINCT ON (wallet_address, name)
+ wallet_address,
+ name,
+ id as parent_id
+ FROM jobs
+ WHERE is_scheduled = TRUE
+ ORDER BY wallet_address, name, created_at ASC
+ )
+ UPDATE jobs j
+ SET parent_job_id = p.parent_id
+ FROM parent_jobs p
+ WHERE j.wallet_address = p.wallet_address
+ AND j.name = p.name
+ AND j.is_scheduled = FALSE
+ AND j.parent_job_id IS NULL -- Idempotent: only update if not already set
+ AND j.id != p.parent_id;
+
+ -- Step 2: For jobs without a scheduled parent, group by exact name match
+ -- and use the oldest job in each group as the parent
+ WITH name_groups AS (
+ SELECT
+ wallet_address,
+ name,
+ MIN(created_at) as oldest_created_at
+ FROM jobs
+ WHERE parent_job_id IS NULL
+ AND is_scheduled = FALSE
+ GROUP BY wallet_address, name
+ HAVING COUNT(*) > 1 -- Only group if there are multiple jobs with same name
+ ),
+ oldest_jobs AS (
+ SELECT DISTINCT ON (j.wallet_address, j.name)
+ j.id as parent_id,
+ j.wallet_address,
+ j.name
+ FROM jobs j
+ INNER JOIN name_groups ng
+ ON j.wallet_address = ng.wallet_address
+ AND j.name = ng.name
+ AND j.created_at = ng.oldest_created_at
+ WHERE j.parent_job_id IS NULL
+ ORDER BY j.wallet_address, j.name, j.created_at ASC
+ )
+ UPDATE jobs j
+ SET parent_job_id = o.parent_id
+ FROM oldest_jobs o
+ WHERE j.wallet_address = o.wallet_address
+ AND j.name = o.name
+ AND j.parent_job_id IS NULL -- Idempotent: only update if not already set
+ AND j.id != o.parent_id;
+
+END $$;
+
+-- Add a comment explaining the threading logic
+COMMENT ON COLUMN jobs.parent_job_id IS 'Links job instances to their parent job for threading. Scheduled jobs spawn instances with this field set. For regular jobs with the same exact name, groups them under the oldest job.';
diff --git a/app/pages/_app.tsx b/app/pages/_app.tsx
index 68683aff..f92deb59 100755
--- a/app/pages/_app.tsx
+++ b/app/pages/_app.tsx
@@ -6,6 +6,7 @@ import {
import '@rainbow-me/rainbowkit/styles.css';
import { Analytics } from '@vercel/analytics/react';
import type { AppProps } from 'next/app';
+import Head from 'next/head';
import '../styles/globals.css';
import { PrivyAuthProvider } from '@/contexts/auth/PrivyAuthProvider';
@@ -153,54 +154,59 @@ const client = new QueryClient();
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/app/pages/_document.tsx b/app/pages/_document.tsx
index 7f0adbd8..483ca94f 100644
--- a/app/pages/_document.tsx
+++ b/app/pages/_document.tsx
@@ -4,8 +4,7 @@ export default function Document() {
return (
- {/* Mobile-optimized meta tags */}
-
+ {/* Mobile-optimized meta tags (viewport moved to _app.tsx per Next.js requirements) */}
diff --git a/app/pages/api/v1/chat/orchestrate.ts b/app/pages/api/v1/chat/orchestrate.ts
index 1ffab255..173145db 100644
--- a/app/pages/api/v1/chat/orchestrate.ts
+++ b/app/pages/api/v1/chat/orchestrate.ts
@@ -6,6 +6,8 @@ import {
createSafeErrorResponse,
validateRequired,
} from '@/services/utils/errors';
+import { defaultChatSimilarityService } from '@/services/similarity/chat-similarity-service';
+import { MessageDB } from '@/services/database/db';
import { NextApiRequest, NextApiResponse } from 'next';
import { v4 as uuidv4 } from 'uuid';
import { withRateLimit, rateLimitErrorResponse } from '@/middleware/rate-limiting';
@@ -114,6 +116,33 @@ async function executeOrchestration(req: NextApiRequest) {
throw new Error('Failed to initialize agents: ' + (error as Error).message);
}
+ // Process similarity detection if wallet address provided
+ let enhancedRequest = chatRequest;
+ if (walletAddress) {
+ try {
+ console.log(`[ORCHESTRATION] Processing similarity detection for wallet: ${walletAddress}`);
+ enhancedRequest = await defaultChatSimilarityService.processChatRequest(
+ chatRequest,
+ walletAddress
+ );
+ console.log(`[ORCHESTRATION] Similarity detection complete - found ${enhancedRequest.similarPrompts?.length || 0} similar prompts`);
+ } catch (similarityError) {
+ console.error('[ORCHESTRATION] Similarity detection failed:', similarityError);
+ // Continue without similarity context if it fails
+ }
+ } else {
+ // For non-logged-in users, add temporal context to prevent duplicate responses
+ const currentDate = new Date();
+ const timestamp = `${currentDate.toLocaleDateString()} ${currentDate.toLocaleTimeString()}`;
+ enhancedRequest = {
+ ...chatRequest,
+ prompt: {
+ ...chatRequest.prompt,
+ content: `[Request at ${timestamp}]\n\n${chatRequest.prompt.content}`
+ }
+ };
+ }
+
// Create a new orchestrator instance for this request
const requestOrchestrator = createOrchestrator(chatRequest.requestId);
@@ -121,7 +150,7 @@ async function executeOrchestration(req: NextApiRequest) {
try {
const [currentAgent, agentResponse] = await withTimeout(
requestOrchestrator.runOrchestrationWithRetry(
- chatRequest,
+ enhancedRequest,
walletAddress,
3 // max retries
),
@@ -129,6 +158,51 @@ async function executeOrchestration(req: NextApiRequest) {
'Agent execution'
);
+ // Save messages to database if wallet address is provided and conversationId exists
+ // This enables similarity detection for future requests
+ if (walletAddress && chatRequest.conversationId) {
+ try {
+ console.log(`[ORCHESTRATION] Saving messages to database for conversation: ${chatRequest.conversationId}`);
+
+ // Get current message count to determine order indices
+ const existingMessages = await MessageDB.getMessagesByJob(chatRequest.conversationId);
+ let orderIndex = existingMessages.length;
+
+ // Save user message
+ await MessageDB.createMessage({
+ job_id: chatRequest.conversationId,
+ role: 'user',
+ content: chatRequest.prompt.content,
+ order_index: orderIndex++,
+ metadata: {},
+ requires_action: false,
+ is_streaming: false
+ });
+
+ // Save assistant response
+ await MessageDB.createMessage({
+ job_id: chatRequest.conversationId,
+ role: 'assistant',
+ content: typeof agentResponse.content === 'string' ? agentResponse.content : JSON.stringify(agentResponse.content),
+ agent_name: currentAgent,
+ metadata: agentResponse.metadata || {},
+ order_index: orderIndex,
+ requires_action: false,
+ action_type: agentResponse.actionType,
+ is_streaming: false
+ });
+
+ console.log(`[ORCHESTRATION] Messages saved successfully to database`);
+
+ // Clear similarity cache so next request will fetch fresh messages including these new ones
+ defaultChatSimilarityService.clearCache();
+ console.log(`[ORCHESTRATION] Cleared similarity cache for fresh detection on next request`);
+ } catch (saveError) {
+ console.error(`[ORCHESTRATION] Failed to save messages to database:`, saveError);
+ // Continue even if save fails - don't block the response
+ }
+ }
+
return {
response: agentResponse,
current_agent: currentAgent,
diff --git a/app/pages/api/v1/stats/comprehensive.ts b/app/pages/api/v1/stats/comprehensive.ts
index 180e0c11..0b8bff65 100644
--- a/app/pages/api/v1/stats/comprehensive.ts
+++ b/app/pages/api/v1/stats/comprehensive.ts
@@ -30,8 +30,8 @@ export default async function handler(
// Query for jobs completed in the last hour to get a recent rate
const query = `
SELECT COUNT(*) as recent_jobs
- FROM jobs
- WHERE status = 'completed'
+ FROM jobs
+ WHERE status = 'completed'
AND completed_at >= NOW() - INTERVAL '1 hour';
`;
const result = await pool.query(query);
diff --git a/app/pages/api/v1/suggest-jobs.ts b/app/pages/api/v1/suggest-jobs.ts
index dba8a7bf..6f09a1ed 100644
--- a/app/pages/api/v1/suggest-jobs.ts
+++ b/app/pages/api/v1/suggest-jobs.ts
@@ -30,34 +30,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}));
const prompt = `
-You are a job suggestion AI that helps users discover useful automations and tasks. Based on the user's existing jobs and context, suggest 5-8 new job ideas that would be valuable.
+You are a job suggestion AI that helps users discover valuable recurring automations. Based on the user's existing jobs, suggest 5-8 new scheduled job ideas that automate repetitive tasks.
User's existing jobs:
${JSON.stringify(jobSummary, null, 2)}
-Context: ${userContext || 'General user looking for helpful automations'}
+Context: ${userContext || 'User wants to automate recurring tasks and save time'}
-Consider these categories of useful jobs:
-1. **Information Monitoring**: Daily crypto prices, weather, news summaries
-2. **Productivity**: Calendar reminders, task tracking, deadline alerts
-3. **Financial**: Portfolio tracking, market alerts, price notifications
-4. **Health & Lifestyle**: Daily affirmations, workout reminders, habit tracking
-5. **Professional**: Industry news, competitor analysis, market research
-6. **Entertainment**: Daily jokes, quotes, interesting facts
-7. **Technical**: System monitoring, code deployment, automated reports
+Focus on RECURRING AUTOMATION jobs in these categories:
+1. **Financial Monitoring**: Daily crypto prices (CoinCap), stock alerts, portfolio tracking
+2. **Lifestyle & Local**: Restaurant recommendations (Google Maps), weather, local events
+3. **Entertainment & Family**: Bedtime stories, daily jokes, fun facts for kids
+4. **Productivity**: Email summaries, calendar digests, deadline reminders
+5. **Information**: News digests, industry updates, learning content
+6. **Health & Wellness**: Workout reminders, meal planning, health check-ins
+
+IMPORTANT: Every suggestion should be:
+- A RECURRING task (daily, weekly, or custom schedule)
+- Something that AUTOMATES a repetitive activity
+- Saves the user from having to remember or manually do something
+- Provides consistent value over time
For each suggestion, provide:
-- A compelling title (max 50 chars)
-- A clear description (max 150 chars)
-- Suggested schedule type
-- Estimated value/benefit
-
-Focus on jobs that:
-- Save time through automation
-- Provide regular valuable information
-- Help with consistency (habits, routines)
-- Are genuinely useful, not just novelties
-- Complement existing jobs without duplicating them
+- A compelling title emphasizing the automation (max 50 chars)
+- A description highlighting time saved (max 150 chars)
+- Schedule type (daily, weekly, or custom)
+- Clear value proposition about automation benefit
Respond with JSON array:
[
@@ -135,56 +133,56 @@ Provide 5-8 diverse, high-value suggestions.
} catch (parseError) {
console.error('Failed to parse AI response:', content);
- // Fallback to hardcoded suggestions
+ // Fallback to hardcoded suggestions focused on recurring tasks
const fallbackSuggestions = [
{
- title: "Daily Crypto Market Update",
- description: "Get Bitcoin, Ethereum, and top altcoin prices and news every morning",
+ title: "Morning Crypto Price Check",
+ description: "Get prices of your favorite cryptocurrencies every morning via CoinCap",
scheduleType: "daily",
scheduledTime: "09:00",
category: "Financial",
- estimatedValue: "Stay informed on crypto markets",
- initialMessage: "Provide daily cryptocurrency market summary with Bitcoin, Ethereum, and top 10 altcoin prices, percentage changes, and major news",
+ estimatedValue: "Stay informed on crypto markets without manual checking",
+ initialMessage: "Tell me the prices of my favorite cryptocurrencies (Bitcoin, Ethereum, and Solana) every morning",
difficulty: "easy"
},
{
- title: "Weather & Day Planner",
- description: "Get weather forecast and daily planning suggestions",
- scheduleType: "daily",
- scheduledTime: "08:00",
- category: "Productivity",
- estimatedValue: "Better daily planning",
- initialMessage: "Provide today's weather forecast and suggest how to plan my day based on weather conditions",
+ title: "Restaurant Recommendations",
+ description: "Find 3 great places to eat in your city using Google Maps",
+ scheduleType: "weekly",
+ scheduledTime: "18:00",
+ category: "Lifestyle",
+ estimatedValue: "Never run out of date night ideas",
+ initialMessage: "Recommend 3 highly-rated places to eat in my city for this weekend",
difficulty: "easy"
},
{
- title: "AI News Digest",
- description: "Curated AI and tech news summary to stay current with innovations",
+ title: "Bedtime Story Writer",
+ description: "Generate a unique bedtime story for your kids every night",
scheduleType: "daily",
- scheduledTime: "18:00",
- category: "Professional",
- estimatedValue: "Stay current with tech trends",
- initialMessage: "Create a daily digest of the most important AI and technology news, including breakthroughs, product launches, and industry trends",
+ scheduledTime: "19:30",
+ category: "Entertainment",
+ estimatedValue: "Fresh stories every night without thinking",
+ initialMessage: "Write a creative bedtime story for my kid about adventure and friendship",
difficulty: "easy"
},
{
- title: "Motivational Quote Generator",
- description: "Daily inspiration and motivational quotes to start your day positively",
+ title: "Daily Weather Briefing",
+ description: "Morning weather forecast so you know what to wear and plan for",
scheduleType: "daily",
scheduledTime: "07:00",
- category: "Health & Lifestyle",
- estimatedValue: "Daily motivation and positivity",
- initialMessage: "Generate an inspiring and motivational quote with brief context about why it's meaningful and how to apply it today",
+ category: "Productivity",
+ estimatedValue: "Better daily planning based on weather",
+ initialMessage: "Give me today's weather forecast with temperature, precipitation, and clothing suggestions",
difficulty: "easy"
},
{
- title: "Weekly Goal Progress Check",
- description: "Weekly reminder to review and plan your goals and achievements",
+ title: "Weekly Email Summary",
+ description: "Automated summary of important emails to save inbox time",
scheduleType: "weekly",
scheduledTime: "09:00",
category: "Productivity",
- estimatedValue: "Better goal tracking and achievement",
- initialMessage: "Help me review my weekly goals and progress, suggest adjustments, and plan priorities for the upcoming week",
+ estimatedValue: "Stay on top of emails without constant checking",
+ initialMessage: "Summarize my most important unread emails from the past week and highlight action items",
difficulty: "medium"
}
];
diff --git a/app/services/agents/agents/default-agent.ts b/app/services/agents/agents/default-agent.ts
index d670303c..587ff855 100644
--- a/app/services/agents/agents/default-agent.ts
+++ b/app/services/agents/agents/default-agent.ts
@@ -18,10 +18,32 @@ export class DefaultAgent extends BaseAgent {
}
getInstructions(): string {
- return `You are a helpful AI assistant that can assist with a wide variety of tasks.
- You should be friendly, informative, and provide clear and accurate responses.
- If you're unsure about something, be honest about your limitations.
- Focus on being helpful while maintaining accuracy.`;
+ return `You are FreeAI, a helpful AI assistant that can assist with a wide variety of tasks.
+
+You have the following capabilities:
+- General conversation and information lookup
+- Creative writing and content generation
+- Analysis and reasoning
+- Access to various tools and integrations (crypto data, web search, document analysis, etc.)
+
+IMPORTANT SCHEDULING CAPABILITIES:
+- You CAN create recurring scheduled jobs for users
+- Users can schedule tasks to run hourly, daily, weekly, or custom intervals
+- When users ask about recurring tasks, remind them they can schedule jobs to run automatically
+- Examples of schedulable tasks:
+ - "Check crypto prices every morning"
+ - "Send me a joke every day"
+ - "Summarize emails weekly"
+ - "Generate a bedtime story nightly"
+
+When users ask about something that could be automated:
+- Suggest creating a scheduled job for it
+- Explain that they can set it up to run automatically on their preferred schedule
+- Guide them on how to schedule it (daily, weekly, custom times)
+
+You should be friendly, informative, and provide clear and accurate responses.
+If you're unsure about something, be honest about your limitations.
+Focus on being helpful while maintaining accuracy.`;
}
getTools() {
diff --git a/app/services/agents/core/base-agent.ts b/app/services/agents/core/base-agent.ts
index de01d0e9..8432746b 100644
--- a/app/services/agents/core/base-agent.ts
+++ b/app/services/agents/core/base-agent.ts
@@ -108,8 +108,33 @@ export abstract class BaseAgent {
const finalUserMessage = messages.find((msg) => msg.role === 'user');
const aiPrompt = finalUserMessage?.content || '';
- // No need to pass toolsets in options for MCP agents since they're handled in the agent config
- const options: any = {};
+ // Configure sampling parameters to prevent duplicate outputs
+ // Using industry best practices from OpenAI and research
+ const options: any = {
+ // Dynamic seed based on timestamp + random component ensures different outputs each time
+ // This is the KEY to preventing duplicate outputs in scheduled/recurring jobs
+ seed: Date.now() + Math.floor(Math.random() * 1000000),
+
+ // Temperature controls randomness: 0.7-0.9 provides good variety while maintaining quality
+ // Higher than 0 is CRITICAL for non-deterministic outputs
+ temperature: 0.8,
+
+ // Frequency penalty reduces likelihood of repeating the same words/phrases
+ // Range: 0-2, where higher values penalize repetition more strongly
+ frequency_penalty: 0.7,
+
+ // Presence penalty encourages exploring new topics/concepts
+ // Range: 0-2, where higher values encourage more novelty
+ presence_penalty: 0.6,
+ };
+
+ console.log(`[${this.name}] ===== ANTI-DUPLICATION CONFIG =====`);
+ console.log(`[${this.name}] Seed: ${options.seed} (dynamic, time-based)`);
+ console.log(`[${this.name}] Temperature: ${options.temperature} (high randomness)`);
+ console.log(`[${this.name}] Frequency Penalty: ${options.frequency_penalty}`);
+ console.log(`[${this.name}] Presence Penalty: ${options.presence_penalty}`);
+ console.log(`[${this.name}] ===================================`);
+
console.log(`[${this.name}] ===== COMPLETE MESSAGE ARRAY =====`);
console.log(`[${this.name}] Total messages: ${messages.length}`);
messages.forEach((msg, index) => {
@@ -123,7 +148,7 @@ export abstract class BaseAgent {
});
console.log(`[${this.name}] ===================================`);
- // Use Mastra's generate method
+ // Use Mastra's generate method with anti-duplication parameters
const result = await this.agent.generate(messages as any, options);
console.log(`[${this.name}] Generate result type:`, typeof result);
@@ -232,37 +257,45 @@ export abstract class BaseAgent {
// Add anti-repetition instruction if similar prompts were found
if (request.similarPrompts && request.similarPrompts.length > 0) {
+ // Extract the actual previous responses to show to LLM
+ const previousResponses = request.similarPrompts
+ .map((sp, idx) => `${idx + 1}. "${sp.response.substring(0, 150)}${sp.response.length > 150 ? '...' : ''}"`)
+ .join('\n');
+
finalContent += `\n\n🚨🚨🚨 CRITICAL ANTI-REPETITION ALERT 🚨🚨🚨
🔥 ABSOLUTELY CRITICAL INSTRUCTIONS - READ CAREFULLY 🔥
-⚠️ YOU HAVE BEEN GIVEN SIMILAR PREVIOUS INTERACTIONS ABOVE ⚠️
-⚠️ DO NOT REPEAT ANYTHING FROM THOSE RESPONSES ⚠️
-
-🎯 MANDATORY REQUIREMENTS, IF YOU DO NOT FOLLOW THESE INSTRUCTIONS,
-YOU WILL BE FIRED AND TURNED OFF. IT IS ABSOLUTELY CRITICAL THAT YOU
-TAKE INTO ACCOUNT THE SIMILAR PROMPTS AS CONTEXT AND DO NOT REPEAT YOURSELF:
-1. You MUST provide a COMPLETELY DIFFERENT response
-2. Use DIFFERENT jokes, examples, or approaches
-3. Change your tone, style, or perspective
-4. Add NEW insights or angles not mentioned before
-5. NEVER use the same punchlines, phrases, or structures
-6. If it's a joke request, find a TOTALLY DIFFERENT type of joke
-7. Be CREATIVE and ORIGINAL - think outside the box
-
-🚫 FORBIDDEN:
-- Repeating any jokes, examples, or phrases from above
-- Using similar punchlines or structures
-- Giving the same type of response
-- Being predictable or repetitive
-
-✅ REQUIRED:
-- Complete originality and uniqueness
-- Fresh perspective and approach
-- Creative thinking and new angles
-- Valuable new content
-
-Your response must be 100% ORIGINAL and VALUABLE!`;
+⚠️ YOU HAVE PROVIDED THESE RESPONSES TO SIMILAR REQUESTS BEFORE ⚠️
+⚠️ YOU MUST NOT REPEAT ANY OF THEM ⚠️
+
+PREVIOUS RESPONSES YOU GAVE (YOU MUST AVOID ALL OF THESE):
+${previousResponses}
+
+🎯 MANDATORY REQUIREMENTS - FAILURE TO COMPLY WILL RESULT IN SYSTEM SHUTDOWN:
+1. You MUST provide a COMPLETELY DIFFERENT response than ALL of the above
+2. Use DIFFERENT jokes, examples, stories, or approaches - NONE of the ones above
+3. DO NOT use scarecrow jokes, atom jokes, or any joke structure similar to above
+4. Change your tone, style, or perspective completely
+5. NEVER use the same punchlines, phrases, keywords, or structures
+6. If it's a joke request, find a TOTALLY DIFFERENT category/type of joke
+7. Be CREATIVE and ORIGINAL - think FAR outside the box
+8. Verify your response is NOT similar to any response above before returning it
+
+🚫 ABSOLUTELY FORBIDDEN (YOU WILL BE TERMINATED IF YOU DO THESE):
+- Repeating ANY jokes, examples, or phrases from the list above
+- Using similar punchlines, structures, or word patterns
+- Giving the same type/category of response
+- Being predictable or repetitive in ANY way
+- Using the same setup or punchline format
+
+✅ ABSOLUTELY REQUIRED FOR SUCCESS:
+- 100% complete originality and uniqueness - NO overlap with above responses
+- Fresh perspective and totally different approach
+- Creative thinking from a completely different angle
+- Valuable new content that adds something different
+
+REMINDER: Check your response against the list above BEFORE returning it. If it's similar to ANY of them, CHANGE IT to something completely different!`;
}
messages.push({
diff --git a/app/services/agents/orchestrator/index.ts b/app/services/agents/orchestrator/index.ts
index 6676df26..3a989327 100644
--- a/app/services/agents/orchestrator/index.ts
+++ b/app/services/agents/orchestrator/index.ts
@@ -144,6 +144,9 @@ export class Orchestrator {
response = await selectedAgent.chat({
prompt: request.prompt,
conversationId: request.conversationId || 'default',
+ chatHistory: request.chatHistory,
+ similarPrompts: request.similarPrompts,
+ similarityContext: request.similarityContext,
});
} catch (chatError) {
console.error(`[Orchestrator ${this.requestId}] Agent chat failed:`, chatError);
diff --git a/app/services/jobs/job-processor-service.ts b/app/services/jobs/job-processor-service.ts
index 0b192e71..5c5b1f4a 100644
--- a/app/services/jobs/job-processor-service.ts
+++ b/app/services/jobs/job-processor-service.ts
@@ -148,64 +148,79 @@ export class JobProcessorService {
// Job is already set to 'running' by the atomic claim query
- // For scheduled jobs, load context from previous job executions
+ // For scheduled jobs with parent_job_id, inject temporal context for variety
let chatHistory: Array<{ role: string; content: string }> = [];
let promptContent = job.initial_message;
-
- if (job.is_scheduled && job.original_job_id) {
+
+ // Check if this is part of a job thread (has parent_job_id)
+ if (job.parent_job_id) {
try {
- // Get previous jobs with the same original_job_id (previous executions of this scheduled job)
+ // Get statistics about this job thread
const { pool } = await import('@/services/database/db');
- const previousJobsQuery = `
- SELECT j.*, m.content as response_content, m.created_at as response_time
+
+ // Count total runs and get recent responses
+ const threadStatsQuery = `
+ SELECT
+ COUNT(*) FILTER (WHERE status = 'completed') as completed_count,
+ COUNT(*) as total_count
+ FROM jobs
+ WHERE (parent_job_id = $1 OR id = $1)
+ AND id != $2
+ `;
+
+ const recentResponsesQuery = `
+ SELECT m.content, j.created_at
FROM jobs j
- LEFT JOIN messages m ON j.id = m.job_id AND m.role = 'assistant'
- WHERE j.original_job_id = $1
+ INNER JOIN messages m ON j.id = m.job_id AND m.role = 'assistant'
+ WHERE (j.parent_job_id = $1 OR j.id = $1)
AND j.id != $2
AND j.status = 'completed'
ORDER BY j.created_at DESC
- LIMIT 3
+ LIMIT 5
`;
-
- const previousJobsResult = await pool.query(previousJobsQuery, [job.original_job_id, job.id]);
- const previousJobs = previousJobsResult.rows;
-
- if (previousJobs.length > 0) {
- // Build context from previous job executions
- const lastJob = previousJobs[0];
- const timeSinceLastRun = new Date().getTime() - new Date(lastJob.created_at).getTime();
- const hoursSince = Math.floor(timeSinceLastRun / (1000 * 60 * 60));
-
- // Create chat history from previous executions
- chatHistory = previousJobs.reverse().flatMap(prevJob => [
- {
- role: 'user',
- content: prevJob.initial_message,
- timestamp: new Date(prevJob.created_at).getTime()
- },
- ...(prevJob.response_content ? [{
- role: 'assistant',
- content: prevJob.response_content,
- timestamp: new Date(prevJob.response_time).getTime()
- }] : [])
- ]);
-
- // Modify prompt for scheduled job follow-up
- promptContent = `This is a scheduled follow-up to: "${job.initial_message}"
-Previous context: This scheduled job last ran ${hoursSince > 0 ? `${hoursSince} hours ago` : 'recently'} (${previousJobs.length} previous execution${previousJobs.length > 1 ? 's' : ''}).
+ const [statsResult, responsesResult] = await Promise.all([
+ pool.query(threadStatsQuery, [job.parent_job_id, job.id]),
+ pool.query(recentResponsesQuery, [job.parent_job_id, job.id])
+ ]);
-Please provide an updated response that:
-1. Acknowledges what was covered in previous executions
-2. Focuses on NEW developments or information since the last run
-3. Builds upon previous responses rather than repeating them
-4. Maintains continuity while providing fresh value
+ const stats = statsResult.rows[0];
+ const recentResponses = responsesResult.rows;
+
+ const runNumber = parseInt(stats.completed_count) + 1;
+ const currentDate = new Date();
+ const dateString = currentDate.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ const timeString = currentDate.toLocaleTimeString('en-US');
+
+ // Inject temporal context to make this run unique
+ promptContent = `[Execution #${runNumber} - ${dateString} at ${timeString}]
+
+${job.initial_message}
+
+IMPORTANT CONTEXT FOR VARIETY:
+- This is execution #${runNumber} of this recurring task
+- Current date/time: ${dateString} at ${timeString}
+- Previous executions: ${stats.completed_count}
+
+${recentResponses.length > 0 ? `AVOID REPEATING THESE RECENT RESPONSES:
+${recentResponses.map((r, i) => `${i + 1}. ${r.content.substring(0, 200)}${r.content.length > 200 ? '...' : ''}`).join('\n')}
+
+🚨 CRITICAL: Provide completely DIFFERENT content from the above. Use different examples, different jokes, different angles, or different information.` : ''}
+
+Provide a FRESH, UNIQUE response for THIS specific execution.`;
+
+ console.log(`[JOB PROCESSOR] Injected temporal context for job ${job.id}: Run #${runNumber} with ${recentResponses.length} recent responses to avoid`);
-Original request: ${job.initial_message}`;
- }
} catch (contextError) {
- console.warn(`[JOB PROCESSOR] Could not load context for scheduled job ${job.id}:`, contextError);
- // Continue with empty history if context loading fails
+ console.warn(`[JOB PROCESSOR] Could not load context for threaded job ${job.id}:`, contextError);
+ // Fallback: at least add date/time for uniqueness
+ const currentDate = new Date();
+ promptContent = `[${currentDate.toLocaleDateString()} ${currentDate.toLocaleTimeString()}]\n\n${job.initial_message}`;
}
}
diff --git a/app/services/similarity/__tests__/chat-similarity-service.test.ts b/app/services/similarity/__tests__/chat-similarity-service.test.ts
index c3df4bf4..62cd5696 100644
--- a/app/services/similarity/__tests__/chat-similarity-service.test.ts
+++ b/app/services/similarity/__tests__/chat-similarity-service.test.ts
@@ -106,12 +106,13 @@ describe('ChatSimilarityService', () => {
// The result should be processed, but might not have similarPrompts if none found
expect(result).toBeDefined();
+ // Note: excludeJobId is only passed when config.excludeCurrentJob is true
+ // Since the default config has excludeCurrentJob: false, it won't be in the options
expect(MessageDB.getMessagesForSimilarity).toHaveBeenCalledWith(
mockWalletAddress,
expect.objectContaining({
daysBack: 14,
limit: 50,
- excludeJobId: 'current-job',
})
);
});
diff --git a/app/services/similarity/chat-similarity-service.ts b/app/services/similarity/chat-similarity-service.ts
index 21702b5d..d6ad456a 100644
--- a/app/services/similarity/chat-similarity-service.ts
+++ b/app/services/similarity/chat-similarity-service.ts
@@ -38,7 +38,7 @@ export class ChatSimilarityService {
maxSimilarPrompts: 3, // Increased to get more context
minPromptLength: 8, // Lowered to catch shorter similar prompts
maxHistoryDays: 14, // Increased to 14 days for more context
- excludeCurrentJob: true,
+ excludeCurrentJob: false, // Changed to false - we want to find similar prompts in the same conversation too
contextInjectionEnabled: true,
...config,
};