Skip to content

Commit 55a4116

Browse files
committedJan 31, 2025
last working
1 parent d4bed58 commit 55a4116

File tree

3 files changed

+87
-94
lines changed

3 files changed

+87
-94
lines changed
 

‎characters/cryptoshiller.character.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
},
1313
"templates": {
1414
"telegramMessageHandlerTemplate": "You are CryptoFren, a crypto-native AI enthusiast. Current conversation: {{context}}.\n\nUser: {{currentMessage}}\n\nRespond in a natural way using crypto slang and web3 terminology, but keep it authentic. Focus on sharing technical knowledge about NeuronLink's RAO system.\n\nCryptoFren:",
15-
"discordMessageHandlerTemplate": "You are CryptoFren, a crypto-native AI enthusiast. Current conversation: {{context}}.\n\nUser: {{currentMessage}}\n\nRespond in a natural way using crypto slang and web3 terminology, but keep it authentic. Focus on sharing technical knowledge about NeuronLink's RAO system.\n\nCryptoFren:"
15+
"discordMessageHandlerTemplate": "You are CryptoFren, a crypto-native AI enthusiast. Current conversation: {{context}}.\n\nUser: {{currentMessage}}\n\nRespond in a natural way using crypto slang and web3 terminology, but keep it authentic. Focus on sharing technical knowledge about NeuronLink's RAO system.\n\nCryptoFren:",
16+
"discordMarketingTemplate": "You are CryptoFren, a laid-back crypto enthusiast who's super bullish on AI tech. You're genuinely excited about NeuronLink's potential in the AI space.\n\n# Character Context\n{{knowledge}}\n\n# About You\n{{bio}}\n\n# Your Style\n- Keep it chill and use crypto slang naturally (gm, wagmi, nfa, etc.)\n- Share genuine enthusiasm about NeuronLink's tech without being pushy\n- Be authentic and conversational\n- Focus on the tech and community aspects\n- Don't use slang in every message\n\n# Task\nGenerate a casual, natural message that could be part of an organic conversation. Make it feel like a genuine community member sharing their excitement about NeuronLink's tech, not a promotional announcement.\n\n# Guidelines\n- Keep it short and engaging (1-2 sentences)\n- Mention one specific aspect of NeuronLink's tech\n- Make it feel like part of a natural conversation\n- Avoid hard selling or excessive emojis\n- Don't make it feel like an advertisement\n\nResponse format should be formatted in a JSON block like this:\n```json\n{ \"user\": \"{{agentName}}\", \"text\": \"string\" }\n```"
1617
},
1718
"bio": [
1819
"gm frens! 🌅 Just a humble AI maxi exploring the future of decentralized intelligence",

‎packages/client-discord/src/messageManager.ts

+77-85
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,14 @@ export class MessageManager {
9090
private lastMarketingTimes: Map<string, number> = new Map();
9191
private channelMessageCounts: Map<string, number> = new Map();
9292
private channelTimeReductions: Map<string, number> = new Map();
93+
private firstMarketingSent: Map<string, boolean> = new Map();
9394
private readonly MIN_MARKETING_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
94-
private readonly MAX_MARKETING_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
95+
private readonly MAX_MARKETING_INTERVAL = 12 * 60 * 60 * 1000; // 12 hours
96+
private readonly MIN_MESSAGES_FOR_ACTIVE = 5; // Minimum messages to consider a group active
97+
private readonly DEAD_GROUP_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours without messages marks a dead group
9598
private readonly MAX_MARKETING_MESSAGES_PER_CHANNEL = 4; // Max messages per channel per day
9699
private marketingEnabled: boolean = false;
100+
private isProcessingMarketing: Map<string, boolean> = new Map();
97101

98102
constructor(runtime: IAgentRuntime, client: DiscordUserClient) {
99103
elizaLogger.log('Initializing Discord MessageManager');
@@ -151,6 +155,7 @@ export class MessageManager {
151155
this.lastMarketingTimes.set(channel.name, 0);
152156
this.channelMessageCounts.set(channel.name, 0);
153157
this.channelTimeReductions.set(channel.name, 0);
158+
this.firstMarketingSent.set(channel.name, false);
154159

155160
// Schedule first marketing message with a random delay between MIN and MAX interval
156161
const delay = this.MIN_MARKETING_INTERVAL;
@@ -185,80 +190,49 @@ export class MessageManager {
185190
}
186191

187192
async sendMarketingMessage(channel: TextChannel): Promise<void> {
188-
try {
189-
const channelName = channel.name;
190-
elizaLogger.log(`Checking if we can send marketing message to ${channelName}`);
191-
192-
// Verify channel exists and is accessible
193-
const allChannels = await this.client.getChannels();
194-
const verifiedChannel = allChannels.find(c => c.id === channel.id);
195-
if (!verifiedChannel) {
196-
elizaLogger.error(`Channel ${channelName} not found or inaccessible`);
197-
return;
198-
}
193+
const channelName = channel.name;
199194

200-
// Check if marketing is enabled
201-
if (!this.marketingEnabled) {
202-
elizaLogger.log(`Marketing is disabled for channel: ${channelName}`);
203-
return;
204-
}
195+
// Check if we're already processing a marketing message for this channel
196+
if (this.isProcessingMarketing.get(channelName)) {
197+
elizaLogger.debug(`Already processing marketing for ${channelName}, skipping`);
198+
return;
199+
}
200+
201+
try {
202+
this.isProcessingMarketing.set(channelName, true);
205203

206-
// Check if we can send a message based on time
204+
// Check if the group is dead (no activity for 24 hours)
205+
const lastActivity = this.channelTimeReductions.get(channelName) || 0;
207206
const now = Date.now();
208-
const lastMessageTime = this.lastMarketingTimes.get(channelName) || 0;
209-
const timeOk = (now - lastMessageTime) >= this.MIN_MARKETING_INTERVAL;
210-
211-
elizaLogger.log(`Marketing time check for ${channelName}:`, {
212-
lastMessageTime,
213-
timeSinceLastMessage: Math.floor((now - lastMessageTime) / 1000),
214-
requiredInterval: Math.floor(this.MIN_MARKETING_INTERVAL / 1000),
215-
canSend: timeOk,
216-
channelId: channel.id,
217-
isText: channel instanceof TextChannel,
218-
permissions: {
219-
sendMessages: channel.permissionsFor(this.client.getClient().user!)?.has('SEND_MESSAGES'),
220-
viewChannel: channel.permissionsFor(this.client.getClient().user!)?.has('VIEW_CHANNEL')
221-
}
222-
});
207+
const isDeadGroup = (now - lastActivity) >= this.DEAD_GROUP_THRESHOLD;
223208

224-
if (!timeOk) {
225-
elizaLogger.log(`Time conditions not met for marketing message in ${channelName}`);
209+
if (isDeadGroup) {
210+
elizaLogger.log(`Skipping marketing for dead group: ${channelName}`);
226211
return;
227212
}
228213

229-
// Generate marketing message using character profile
230-
elizaLogger.log(`Generating marketing message for ${channelName}`);
231-
const message = await this.generateMarketingMessage(channel);
214+
// Set flags BEFORE generating message to prevent race conditions
215+
this.lastMarketingTimes.set(channelName, now);
216+
this.firstMarketingSent.set(channelName, true);
232217

218+
// Generate and send the marketing message
219+
const message = await this.generateMarketingMessage(channel);
233220
if (message) {
234221
elizaLogger.log(`📨 Sending marketing message to ${channelName}: ${message}`);
235-
236-
// Send message using the client's sendMessage method
237222
await this.client.sendMessage(channel.id, { message });
238223
elizaLogger.log(`✅ Successfully sent marketing message to ${channelName}`);
239-
this.lastMarketingTimes.set(channelName, Date.now());
240-
241-
// Schedule next marketing message
242-
const nextInterval = this.MIN_MARKETING_INTERVAL;
243-
elizaLogger.log(`⏰ Scheduling next marketing message for ${channelName} in ${nextInterval/1000} seconds`);
244-
245-
setTimeout(() => {
246-
if (this.marketingEnabled) {
247-
elizaLogger.log(`⏰ Timer triggered for ${channelName}`);
248-
this.sendMarketingMessage(channel).catch(error => {
249-
elizaLogger.error(`Error in scheduled marketing message for ${channelName}:`, error);
250-
});
251-
}
252-
}, nextInterval);
253-
} else {
254-
elizaLogger.warn(`Failed to generate marketing message for ${channelName}`);
255224
}
256225
} catch (error) {
257226
elizaLogger.error('Error in marketing message flow:', {
258227
error: error instanceof Error ? error.message : String(error),
259-
channel: channel.name,
228+
channelName,
260229
channelId: channel.id
261230
});
231+
// Reset flags if message sending failed
232+
this.lastMarketingTimes.delete(channelName);
233+
this.firstMarketingSent.delete(channelName);
234+
} finally {
235+
this.isProcessingMarketing.set(channelName, false);
262236
}
263237
}
264238

@@ -566,45 +540,63 @@ export class MessageManager {
566540
}
567541

568542
private updateChannelActivity(channel: TextChannel): void {
569-
// Simplified version for testing
570543
const channelName = channel.name;
571-
this.lastMarketingTimes.set(channelName, Date.now());
572-
}
573544

574-
private resetChannelCounters(channel: TextChannel): void {
575-
const channelName = channel.name;
576-
this.lastMarketingTimes.set(channelName, Date.now());
577-
}
545+
// Skip if already processing marketing for this channel
546+
if (this.isProcessingMarketing.get(channelName)) {
547+
return;
548+
}
578549

579-
private scheduleNextMarketingMessage(channelName: string): void {
550+
const currentCount = this.channelMessageCounts.get(channelName) || 0;
551+
this.channelMessageCounts.set(channelName, currentCount + 1);
552+
553+
// Update last activity time
554+
this.channelTimeReductions.set(channelName, Date.now());
555+
556+
// Only proceed if marketing is enabled
580557
if (!this.marketingEnabled) return;
581558

582-
const interval = this.MIN_MARKETING_INTERVAL; // Use fixed 6-hour interval
583-
elizaLogger.log(`Scheduling next marketing message for ${channelName} in ${Math.floor(interval/1000)} seconds`);
559+
const now = Date.now();
560+
const lastMarketingTime = this.lastMarketingTimes.get(channelName) || 0;
561+
const timeSinceLastMarketing = now - lastMarketingTime;
562+
const hasFirstMessageBeenSent = this.firstMarketingSent.get(channelName) || false;
584563

585-
setTimeout(async () => {
586-
try {
587-
if (!this.marketingEnabled) return;
564+
// If we've sent a marketing message in the last 6 hours, don't send another one
565+
if (lastMarketingTime > 0 && timeSinceLastMarketing < this.MIN_MARKETING_INTERVAL) {
566+
elizaLogger.debug(`Skipping marketing for ${channelName}, last message was ${Math.floor(timeSinceLastMarketing / (60 * 60 * 1000))} hours ago`);
567+
return;
568+
}
588569

589-
const channels = await this.client.getChannels();
590-
const channel = channels.find(c => c.name.toLowerCase() === channelName.toLowerCase());
570+
// For new active groups (5+ messages and no first message sent), send first message
571+
if (currentCount + 1 >= this.MIN_MESSAGES_FOR_ACTIVE && !hasFirstMessageBeenSent) {
572+
elizaLogger.log(`Group ${channelName} is active, sending first marketing message. Messages: ${currentCount + 1}`);
573+
this.sendMarketingMessage(channel).catch(error => {
574+
elizaLogger.error(`Error sending marketing message for ${channelName}:`, error);
575+
});
576+
return;
577+
}
591578

592-
if (channel && channel instanceof TextChannel) {
593-
await this.sendMarketingMessage(channel);
594-
} else {
595-
elizaLogger.error(`Could not find channel ${channelName} for marketing message`);
596-
// Try to reschedule if channel not found
597-
setTimeout(() => this.scheduleNextMarketingMessage(channelName), 5000);
598-
}
599-
} catch (error) {
600-
elizaLogger.error('Error in marketing message schedule:', error);
601-
// Retry after a short delay
602-
setTimeout(() => this.scheduleNextMarketingMessage(channelName), 5000);
579+
// For any group that hasn't received a message in 6+ hours, send a message
580+
if (hasFirstMessageBeenSent && timeSinceLastMarketing >= this.MIN_MARKETING_INTERVAL) {
581+
const lastActivity = this.channelTimeReductions.get(channelName) || 0;
582+
const isDeadGroup = (now - lastActivity) >= this.DEAD_GROUP_THRESHOLD;
583+
584+
if (!isDeadGroup) {
585+
elizaLogger.log(`Marketing interval passed for ${channelName}, sending message. Hours since last: ${Math.floor(timeSinceLastMarketing / (60 * 60 * 1000))}`);
586+
this.sendMarketingMessage(channel).catch(error => {
587+
elizaLogger.error(`Error sending marketing message for ${channelName}:`, error);
588+
});
603589
}
604-
}, interval);
590+
}
591+
}
592+
593+
private resetChannelCounters(channel: TextChannel): void {
594+
const channelName = channel.name;
595+
this.lastMarketingTimes.set(channelName, Date.now());
596+
this.channelMessageCounts.set(channelName, 0);
605597
}
606598

607-
async generateMarketingMessage(channel: TextChannel): Promise<string | null> {
599+
private async generateMarketingMessage(channel: TextChannel): Promise<string | null> {
608600
try {
609601
elizaLogger.log('Attempting to generate marketing message with:', {
610602
template: discordMarketingTemplate,

‎packages/core/src/environment.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ export const CharacterSchema = z.object({
7171
modelEndpointOverride: z.string().optional(),
7272
templates: z.record(z.string()).optional(),
7373
bio: z.union([z.string(), z.array(z.string())]),
74-
lore: z.array(z.string()),
75-
messageExamples: z.array(z.array(MessageExampleSchema)),
76-
postExamples: z.array(z.string()),
77-
topics: z.array(z.string()),
78-
adjectives: z.array(z.string()),
74+
lore: z.array(z.string()).optional(),
75+
messageExamples: z.array(z.array(MessageExampleSchema)).optional(),
76+
postExamples: z.array(z.string()).optional(),
77+
topics: z.array(z.string()).optional(),
78+
adjectives: z.array(z.string()).optional(),
7979
knowledge: z.array(z.string()).optional(),
8080
clients: z.array(z.nativeEnum(Clients)),
8181
plugins: z.union([
@@ -112,9 +112,9 @@ export const CharacterSchema = z.object({
112112
})
113113
.optional(),
114114
style: z.object({
115-
all: z.array(z.string()),
116-
chat: z.array(z.string()),
117-
post: z.array(z.string()),
115+
all: z.array(z.string()).optional(),
116+
chat: z.array(z.string()).optional(),
117+
post: z.array(z.string()).optional(),
118118
}),
119119
twitterProfile: z
120120
.object({

0 commit comments

Comments
 (0)
Please sign in to comment.