diff --git a/plugin-nostr/lib/selfReflection.js b/plugin-nostr/lib/selfReflection.js index c0840e9..3ba1d8f 100644 --- a/plugin-nostr/lib/selfReflection.js +++ b/plugin-nostr/lib/selfReflection.js @@ -44,6 +44,9 @@ class SelfReflectionEngine { .find((value) => Number.isFinite(value) && value > 0); this.maxTokens = maxTokens || DEFAULT_MAX_TOKENS; + const zapCorrelationSetting = runtime?.getSetting?.('NOSTR_SELF_REFLECTION_ZAP_CORRELATION_ENABLE'); + this.zapCorrelationEnabled = String(zapCorrelationSetting ?? 'true').toLowerCase() === 'true'; + this._systemContext = null; this._systemContextPromise = null; this.lastAnalysis = null; @@ -244,7 +247,7 @@ class SelfReflectionEngine { const conversation = this._buildConversationWindow(roomMemories, memory, parentMemory); const feedback = this._collectFeedback(conversation, memory.id); const timeWindow = this._deriveTimeWindow(conversation, memory.createdAt, parentMemory.createdAt); - const signals = this._collectSignalsForInteraction(sortedMemories, memory, timeWindow); + const signals = await this._collectSignalsForInteraction(sortedMemories, memory, timeWindow); const pubkey = parentMemory.content?.event?.pubkey; let engagementSummary = 'unknown'; @@ -991,7 +994,7 @@ class SelfReflectionEngine { }; } - _collectSignalsForInteraction(allMemories, replyMemory, timeWindow) { + async _collectSignalsForInteraction(allMemories, replyMemory, timeWindow) { if (!Array.isArray(allMemories) || !allMemories.length) { return []; } @@ -1016,17 +1019,52 @@ class SelfReflectionEngine { continue; } - const text = this._truncate( - String( - memory.content?.text || - memory.content?.data?.summary || - memory.content?.data?.text || - '' - ), - 200 - ); + let signalText = ''; + + // Special handling for zap_thanks with correlation + if (typeLabel === 'zap_thanks' && this.zapCorrelationEnabled) { + const targetEventId = memory.content?.data?.targetEventId; + if (targetEventId && this.runtime?.getMemoryById) { + try { + const targetMemory = await this.runtime.getMemoryById(targetEventId); + if (targetMemory?.content) { + const targetText = String( + targetMemory.content?.text || + targetMemory.content?.event?.content || + targetMemory.content?.data?.text || + '' + ).trim(); + + if (targetText) { + const truncatedTarget = this._truncate(targetText, 150); + const zapText = this._truncate( + String(memory.content?.text || ''), + 100 + ); + signalText = `zap_thanks to "${truncatedTarget}": ${zapText}`; + } + } + } catch (err) { + this.logger.debug(`[SELF-REFLECTION] Failed to fetch target post ${targetEventId}:`, err?.message || err); + } + } + } + + // Fallback to original format if correlation didn't work + if (!signalText) { + const text = this._truncate( + String( + memory.content?.text || + memory.content?.data?.summary || + memory.content?.data?.text || + '' + ), + 200 + ); + signalText = `${typeLabel}: ${text}`.trim(); + } - signals.push(`${typeLabel}: ${text}`.trim()); + signals.push(signalText); if (signals.length >= 5) { break; } @@ -1243,8 +1281,9 @@ ${signalLines}`; 3. Are you balancing brevity with substance? Note instances of over-verbosity or curt replies. 4. Call out any repeated phrases, tonal habits, or narrative crutches (good or bad). 5. Compare against prior self-reflection recommendations: where did you improve or regress? -6. Consider the longitudinal analysis: Are recurring issues being addressed? Are persistent strengths being maintained? -7. Surface actionable adjustments for tone, structure, or strategy across future interactions. + 6. Consider the longitudinal analysis: Are recurring issues being addressed? Are persistent strengths being maintained? + 7. Evaluate zaps received on specific posts and what content patterns drove them. Identify what types of content consistently attract zaps vs. what gets ignored. + 8. Surface actionable adjustments for tone, structure, or strategy across future interactions. CRITICAL: For each interaction, provide SPECIFIC behavioral changes: - Quote exact phrases from your replies that need improvement diff --git a/plugin-nostr/test/selfReflection.zap-correlation.test.js b/plugin-nostr/test/selfReflection.zap-correlation.test.js new file mode 100644 index 0000000..83db534 --- /dev/null +++ b/plugin-nostr/test/selfReflection.zap-correlation.test.js @@ -0,0 +1,346 @@ +const { SelfReflectionEngine } = require('../lib/selfReflection'); + +describe('SelfReflectionEngine zap correlation', () => { + let engine; + let mockRuntime; + let mockMemories; + + beforeEach(() => { + mockMemories = []; + + mockRuntime = { + getSetting: (key) => { + if (key === 'NOSTR_SELF_REFLECTION_ZAP_CORRELATION_ENABLE') { + return 'true'; // enabled by default in tests + } + return null; + }, + agentId: 'test-agent-id', + getMemories: async ({ roomId, count }) => { + return mockMemories.slice(0, count); + }, + getMemoryById: async (id) => { + // Will be overridden in specific tests + return null; + }, + createMemory: async (memory) => ({ created: true, id: memory.id }) + }; + + engine = new SelfReflectionEngine(mockRuntime, console, { + createUniqueUuid: (runtime, seed) => `uuid-${seed}-${Date.now()}` + }); + }); + + describe('_collectSignalsForInteraction with zap correlation', () => { + it('includes target post snippet in zap_thanks signals when correlation enabled', async () => { + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks for the zap!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: '⚡ 2100 sats gratitude burst', + data: { + targetEventId: 'target-post-123' + } + } + } + ]; + + // Mock successful fetch of target post + mockRuntime.getMemoryById = async (id) => { + if (id === 'target-post-123') { + return { + id: 'target-post-123', + content: { + text: 'This is an amazing post about AI and creativity that deserves recognition!' + } + }; + } + return null; + }; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toContain('zap_thanks to "This is an amazing post about AI and creativity that deserves recognition!": ⚡ 2100 sats gratitude burst'); + }); + + it('truncates long target post content in zap signals', async () => { + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: 'Thanks for the sats!', + data: { + targetEventId: 'target-post-456' + } + } + } + ]; + + // Mock fetch of very long target post + const longText = 'A'.repeat(300) + ' short ending'; + mockRuntime.getMemoryById = async (id) => { + if (id === 'target-post-456') { + return { + id: 'target-post-456', + content: { + text: longText + } + }; + } + return null; + }; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toContain('zap_thanks to "'); + expect(signals[0]).toContain('…": Thanks for the sats!'); + expect(signals[0].length).toBeLessThan(250); // Should be truncated + }); + + it('falls back to original behavior when targetEventId is missing', async () => { + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: '⚡ 1000 sats thanks' + // No targetEventId + } + } + ]; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toBe('zap_thanks: ⚡ 1000 sats thanks'); + }); + + it('falls back to original behavior when target post fetch fails', async () => { + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: '⚡ 500 sats thanks', + data: { + targetEventId: 'target-post-789' + } + } + } + ]; + + // Mock failed fetch + mockRuntime.getMemoryById = async (id) => { + throw new Error('Post not found'); + }; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toBe('zap_thanks: ⚡ 500 sats thanks'); + }); + + it('skips zap correlation when disabled via config', async () => { + mockRuntime.getSetting = (key) => { + if (key === 'NOSTR_SELF_REFLECTION_ZAP_CORRELATION_ENABLE') { + return 'false'; + } + return null; + }; + + // Re-create engine with disabled correlation + engine = new SelfReflectionEngine(mockRuntime, console, { + createUniqueUuid: (runtime, seed) => `uuid-${seed}-${Date.now()}` + }); + + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: '⚡ 2000 sats thanks', + data: { + targetEventId: 'target-post-999' + } + } + } + ]; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toBe('zap_thanks: ⚡ 2000 sats thanks'); + }); + + it('handles zap_thanks memories without data property', async () => { + const replyMemory = { + id: 'reply-1', + createdAt: Date.now(), + content: { text: 'Thanks!' } + }; + + const timeWindow = { + start: Date.now() - 60000, + end: Date.now() + 60000 + }; + + const allMemories = [ + { + id: 'zap-1', + createdAt: Date.now() - 30000, + content: { + type: 'zap_thanks', + text: '⚡ 1500 sats thanks' + // No data property at all + } + } + ]; + + const signals = await engine._collectSignalsForInteraction(allMemories, replyMemory, timeWindow); + + expect(signals.length).toBe(1); + expect(signals[0]).toBe('zap_thanks: ⚡ 1500 sats thanks'); + }); + }); + + describe('_buildPrompt with zap correlation analysis', () => { + it('includes zap correlation analysis instructions in prompt', () => { + const interactions = [ + { + userMessage: 'Great post!', + yourReply: 'Thanks!', + engagement: 'avg=0.8', + conversation: [], + feedback: [], + signals: ['zap_thanks to "Amazing content about AI": ⚡ 1000 sats thanks'], + metadata: { createdAtIso: '2025-10-05T10:00:00.000Z' } + } + ]; + + const prompt = engine._buildPrompt(interactions); + + expect(prompt).toContain('Evaluate zaps received on specific posts'); + expect(prompt).toContain('what content patterns drove them'); + }); + + it('includes correlated zap signals in interaction details', () => { + const interactions = [ + { + userMessage: 'Loved your art!', + yourReply: 'Glad you liked it!', + engagement: 'avg=0.9', + conversation: [], + feedback: [], + signals: ['zap_thanks to "Beautiful digital artwork": ⚡ 2000 sats amazing work'], + metadata: { createdAtIso: '2025-10-05T10:00:00.000Z' } + } + ]; + + const prompt = engine._buildPrompt(interactions); + + expect(prompt).toContain('zap_thanks to "Beautiful digital artwork": ⚡ 2000 sats amazing work'); + }); + }); + + describe('configuration handling', () => { + it('defaults to enabled when config not set', () => { + mockRuntime.getSetting = () => null; + + const engine = new SelfReflectionEngine(mockRuntime, console, {}); + + // Test that correlation is enabled by default + // This is tested implicitly through the other tests + expect(engine).toBeDefined(); + }); + + it('respects explicit enable setting', () => { + mockRuntime.getSetting = (key) => { + if (key === 'NOSTR_SELF_REFLECTION_ZAP_CORRELATION_ENABLE') { + return 'true'; + } + return null; + }; + + const engine = new SelfReflectionEngine(mockRuntime, console, {}); + + expect(engine).toBeDefined(); + }); + + it('respects explicit disable setting', () => { + mockRuntime.getSetting = (key) => { + if (key === 'NOSTR_SELF_REFLECTION_ZAP_CORRELATION_ENABLE') { + return 'false'; + } + return null; + }; + + const engine = new SelfReflectionEngine(mockRuntime, console, {}); + + expect(engine).toBeDefined(); + }); + }); +}); \ No newline at end of file