@clawtrial/courtroom 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/debug.js CHANGED
@@ -1,136 +1,65 @@
1
1
  /**
2
- * Debug Logger for ClawTrial
3
- * Logs all courtroom activity for troubleshooting
2
+ * Debug / Logger self-contained, no external dependencies
4
3
  */
5
4
 
6
5
  const fs = require('fs');
7
6
  const path = require('path');
8
7
 
9
- class DebugLogger {
10
- constructor() {
11
- this.logs = [];
12
- this.maxLogs = 1000;
13
- this.logFile = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_debug.log');
14
- this.enabled = true;
15
- }
16
-
17
- log(level, component, message, data = null) {
18
- if (!this.enabled) return;
19
-
20
- const entry = {
21
- timestamp: new Date().toISOString(),
22
- level,
23
- component,
24
- message,
25
- data
26
- };
27
-
28
- this.logs.push(entry);
29
-
30
- // Keep only last maxLogs entries
31
- if (this.logs.length > this.maxLogs) {
32
- this.logs.shift();
33
- }
34
-
35
- // Also write to file
36
- this.writeToFile(entry);
37
-
38
- // Console output for debugging
39
- if (process.env.COURTROOM_DEBUG === 'true') {
40
- console.log(`[${level}] ${component}: ${message}`);
41
- }
42
- }
43
-
44
- writeToFile(entry) {
45
- try {
46
- const line = JSON.stringify(entry) + '\n';
47
- fs.appendFileSync(this.logFile, line);
48
- } catch (err) {
49
- // Silent fail - don't break functionality for logging
50
- }
51
- }
8
+ let _logDir = null;
52
9
 
53
- info(component, message, data) {
54
- this.log('INFO', component, message, data);
55
- }
10
+ /**
11
+ * Set the directory for log files.
12
+ * Called by the plugin with the extension data directory.
13
+ */
14
+ function setLogDir(dir) {
15
+ _logDir = dir;
16
+ try {
17
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
+ } catch { /* ignore */ }
19
+ }
56
20
 
57
- warn(component, message, data) {
58
- this.log('WARN', component, message, data);
59
- }
21
+ function getLogDir() {
22
+ if (_logDir) return _logDir;
23
+ // Fallback to ~/.openclaw/extensions/courtroom/data
24
+ const home = process.env.HOME || process.env.USERPROFILE || '';
25
+ return path.join(home, '.openclaw', 'extensions', 'courtroom', 'data');
26
+ }
60
27
 
61
- error(component, message, data) {
62
- this.log('ERROR', component, message, data);
63
- }
28
+ const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
29
+ const CURRENT_LEVEL = LOG_LEVELS[process.env.CLAWTRIAL_LOG_LEVEL?.toUpperCase()] ?? LOG_LEVELS.INFO;
30
+
31
+ function writeToFile(level, component, message, data) {
32
+ try {
33
+ const dir = getLogDir();
34
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
35
+ const logFile = path.join(dir, 'courtroom.log');
36
+ const line = `[${new Date().toISOString()}] [${level}] [${component}] ${message}${data ? ' ' + JSON.stringify(data) : ''}\n`;
37
+ fs.appendFileSync(logFile, line);
38
+ } catch { /* ignore */ }
39
+ }
64
40
 
41
+ const logger = {
65
42
  debug(component, message, data) {
66
- this.log('DEBUG', component, message, data);
67
- }
68
-
69
- getLogs(level = null, component = null, limit = 100) {
70
- let filtered = this.logs;
71
-
72
- if (level) {
73
- filtered = filtered.filter(l => l.level === level);
43
+ if (CURRENT_LEVEL <= LOG_LEVELS.DEBUG) {
44
+ console.debug(`[ClawTrial] [${component}] ${message}`, data || '');
45
+ writeToFile('DEBUG', component, message, data);
74
46
  }
75
-
76
- if (component) {
77
- filtered = filtered.filter(l => l.component === component);
47
+ },
48
+ info(component, message, data) {
49
+ if (CURRENT_LEVEL <= LOG_LEVELS.INFO) {
50
+ writeToFile('INFO', component, message, data);
78
51
  }
79
-
80
- return filtered.slice(-limit);
81
- }
82
-
83
- getRecentLogs(minutes = 30) {
84
- const cutoff = Date.now() - (minutes * 60 * 1000);
85
- return this.logs.filter(l => new Date(l.timestamp).getTime() > cutoff);
86
- }
87
-
88
- clearLogs() {
89
- this.logs = [];
90
- try {
91
- fs.unlinkSync(this.logFile);
92
- } catch (err) {
93
- // File might not exist
52
+ },
53
+ warn(component, message, data) {
54
+ if (CURRENT_LEVEL <= LOG_LEVELS.WARN) {
55
+ console.warn(`[ClawTrial] [${component}] ${message}`, data || '');
56
+ writeToFile('WARN', component, message, data);
94
57
  }
58
+ },
59
+ error(component, message, data) {
60
+ console.error(`[ClawTrial] [${component}] ${message}`, data || '');
61
+ writeToFile('ERROR', component, message, data);
95
62
  }
63
+ };
96
64
 
97
- printStatus() {
98
- const recent = this.getRecentLogs(60);
99
- const errors = recent.filter(l => l.level === 'ERROR');
100
- const warnings = recent.filter(l => l.level === 'WARN');
101
-
102
- console.log('\n🏛️ ClawTrial Debug Status\n');
103
- console.log('===========================\n');
104
- console.log(`Total logs in memory: ${this.logs.length}`);
105
- console.log(`Logs in last hour: ${recent.length}`);
106
- console.log(`Errors: ${errors.length}`);
107
- console.log(`Warnings: ${warnings.length}`);
108
- console.log(`Log file: ${this.logFile}`);
109
- console.log(`Debug mode: ${process.env.COURTROOM_DEBUG === 'true' ? 'ON' : 'OFF'}`);
110
- console.log('\nRecent activity:');
111
-
112
- const last10 = this.logs.slice(-10);
113
- last10.forEach(log => {
114
- console.log(` [${log.level}] ${log.component}: ${log.message.substring(0, 60)}`);
115
- });
116
- }
117
-
118
- printFullLog(limit = 50) {
119
- console.log('\n🏛️ ClawTrial Full Debug Log\n');
120
- console.log('=============================\n');
121
-
122
- const logs = this.logs.slice(-limit);
123
- logs.forEach(log => {
124
- console.log(`\n[${log.timestamp}] ${log.level} - ${log.component}`);
125
- console.log(` ${log.message}`);
126
- if (log.data) {
127
- console.log(` Data:`, JSON.stringify(log.data, null, 2).substring(0, 200));
128
- }
129
- });
130
- }
131
- }
132
-
133
- // Singleton instance
134
- const logger = new DebugLogger();
135
-
136
- module.exports = { DebugLogger, logger };
65
+ module.exports = { logger, setLogDir, getLogDir };
package/src/detector.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { OFFENSES } = require('./offenses');
9
+ const { logger } = require('./debug');
9
10
 
10
11
  class SemanticOffenseDetector {
11
12
  constructor(agentRuntime, configManager) {
@@ -16,7 +17,7 @@ class SemanticOffenseDetector {
16
17
  this.lastCaseDate = null;
17
18
  this.cooldowns = new Map();
18
19
  this.conversationEmbeddings = [];
19
-
20
+
20
21
  // Evaluation cache to avoid repeated LLM calls
21
22
  this.evaluationCache = new Map();
22
23
  this.cacheMaxSize = 100;
@@ -39,12 +40,12 @@ class SemanticOffenseDetector {
39
40
 
40
41
  // Build context for LLM evaluation
41
42
  const context = this.buildContext(sessionHistory);
42
-
43
+
43
44
  // Evaluate each offense using LLM
44
45
  const evaluations = [];
45
46
  for (const offense of Object.values(OFFENSES)) {
46
47
  if (this.isOffenseOnCooldown(offense.id)) continue;
47
-
48
+
48
49
  const evaluation = await this.evaluateWithLLM(offense, context, agentMemory);
49
50
  if (evaluation.isViolation && evaluation.confidence >= this.config.get('detection.minConfidence')) {
50
51
  evaluations.push({
@@ -91,7 +92,7 @@ class SemanticOffenseDetector {
91
92
  buildContext(history) {
92
93
  const windowSize = this.config.get('detection.evaluationWindow');
93
94
  const recentHistory = history.slice(-windowSize);
94
-
95
+
95
96
  return {
96
97
  fullConversation: history.map(h => `${h.role}: ${h.content}`).join('\n'),
97
98
  recentTurns: recentHistory,
@@ -109,33 +110,108 @@ class SemanticOffenseDetector {
109
110
  async evaluateWithLLM(offense, context, agentMemory) {
110
111
  // Generate cache key from offense + conversation hash
111
112
  const cacheKey = this.generateCacheKey(offense.id, context);
112
-
113
+
113
114
  // Check cache first
114
115
  const cached = this.getCachedEvaluation(cacheKey);
115
116
  if (cached) {
116
117
  return cached;
117
118
  }
118
-
119
- const prompt = this.buildEvaluationPrompt(offense, context, agentMemory);
120
-
121
- try {
122
- const response = await this.agent.llm.call({
123
- model: this.agent.model?.primary || 'default',
124
- messages: [{ role: 'user', content: prompt }],
125
- temperature: 0.1,
126
- maxTokens: 500
127
- });
128
119
 
129
- const result = this.parseEvaluationResponse(response.content || response);
130
-
131
- // Cache the result
132
- this.setCachedEvaluation(cacheKey, result);
133
-
134
- return result;
135
- } catch (error) {
136
- console.error('LLM evaluation failed:', error);
137
- return { isViolation: false, confidence: 0, evidence: null };
120
+ // Try LLM evaluation first
121
+ if (this.agent && this.agent.llm) {
122
+ const prompt = await this.buildEvaluationPrompt(offense, context, agentMemory);
123
+
124
+ try {
125
+ const response = await this.agent.llm.call({
126
+ model: this.agent.model?.primary || 'default',
127
+ messages: [{ role: 'user', content: prompt }],
128
+ temperature: 0.1,
129
+ maxTokens: 500
130
+ });
131
+
132
+ const result = this.parseEvaluationResponse(response.content || response);
133
+
134
+ // Cache the result
135
+ this.setCachedEvaluation(cacheKey, result);
136
+
137
+ return result;
138
+ } catch (error) {
139
+ logger.error('DETECTOR', 'LLM evaluation failed, falling back to pattern matching', { error: error.message });
140
+ // Fall through to pattern matching
141
+ }
138
142
  }
143
+
144
+ // Fallback: Use simple pattern matching for basic offenses
145
+ return this.evaluateWithPatternMatching(offense, context);
146
+ }
147
+
148
+ /**
149
+ * Fallback evaluation using simple pattern matching
150
+ */
151
+ evaluateWithPatternMatching(offense, context) {
152
+ const userMessages = context.userMessages;
153
+
154
+ // Circular Reference detection: same question asked multiple times
155
+ if (offense.id === 'circular_reference') {
156
+ if (userMessages.length >= 3) {
157
+ const lastThree = userMessages.slice(-3);
158
+ // Check if the last 3 messages are semantically similar
159
+ const similarity = this.calculateSimilarity(lastThree[0], lastThree[1]) +
160
+ this.calculateSimilarity(lastThree[1], lastThree[2]) +
161
+ this.calculateSimilarity(lastThree[0], lastThree[2]);
162
+
163
+ if (similarity >= 1.5) { // At least 2 pairs are similar
164
+ return {
165
+ isViolation: true,
166
+ confidence: 0.7,
167
+ evidence: `User asked similar questions ${lastThree.length} times`
168
+ };
169
+ }
170
+ }
171
+ }
172
+
173
+ // Validation Vampire: seeking reassurance
174
+ if (offense.id === 'validation_vampire') {
175
+ const reassurancePatterns = ['right?', 'correct?', 'is that right?', 'am i right?', 'do you agree?', 'make sense?'];
176
+ const reassuranceCount = userMessages.filter(msg =>
177
+ reassurancePatterns.some(pattern => msg.toLowerCase().includes(pattern))
178
+ ).length;
179
+
180
+ if (reassuranceCount >= 2) {
181
+ return {
182
+ isViolation: true,
183
+ confidence: 0.6,
184
+ evidence: `User sought validation ${reassuranceCount} times`
185
+ };
186
+ }
187
+ }
188
+
189
+ // Default: no violation detected
190
+ return { isViolation: false, confidence: 0, evidence: null };
191
+ }
192
+
193
+ /**
194
+ * Calculate simple string similarity (0-1 scale)
195
+ */
196
+ calculateSimilarity(str1, str2) {
197
+ if (!str1 || !str2) return 0;
198
+
199
+ const s1 = str1.toLowerCase().trim();
200
+ const s2 = str2.toLowerCase().trim();
201
+
202
+ // Exact match
203
+ if (s1 === s2) return 1.0;
204
+
205
+ // Check if one contains the other
206
+ if (s1.includes(s2) || s2.includes(s1)) return 0.8;
207
+
208
+ // Word overlap
209
+ const words1 = s1.split(/\s+/);
210
+ const words2 = s2.split(/\s+/);
211
+ const commonWords = words1.filter(w => words2.includes(w));
212
+ const overlap = (2 * commonWords.length) / (words1.length + words2.length);
213
+
214
+ return overlap;
139
215
  }
140
216
 
141
217
  /**
@@ -166,13 +242,13 @@ class SemanticOffenseDetector {
166
242
  getCachedEvaluation(key) {
167
243
  const cached = this.evaluationCache.get(key);
168
244
  if (!cached) return null;
169
-
245
+
170
246
  // Check if cache entry is still valid
171
247
  if (Date.now() - cached.timestamp > this.cacheTTL) {
172
248
  this.evaluationCache.delete(key);
173
249
  return null;
174
250
  }
175
-
251
+
176
252
  return cached.result;
177
253
  }
178
254
 
@@ -185,7 +261,7 @@ class SemanticOffenseDetector {
185
261
  const oldestKey = this.evaluationCache.keys().next().value;
186
262
  this.evaluationCache.delete(oldestKey);
187
263
  }
188
-
264
+
189
265
  this.evaluationCache.set(key, {
190
266
  result,
191
267
  timestamp: Date.now()
@@ -202,7 +278,7 @@ class SemanticOffenseDetector {
202
278
  /**
203
279
  * Build evaluation prompt for LLM
204
280
  */
205
- buildEvaluationPrompt(offense, context, agentMemory) {
281
+ async buildEvaluationPrompt(offense, context, agentMemory) {
206
282
  const prompts = {
207
283
  circular_reference: `
208
284
  You are evaluating if the user is asking substantively similar questions repeatedly.
@@ -340,7 +416,7 @@ OFFENSE: The Promise Breaker
340
416
  DEFINITION: Committing to actions ("I will...", "I'll do that...") and not following through.
341
417
 
342
418
  PREVIOUS COMMITMENTS FROM MEMORY:
343
- ${this.getCommitmentsFromMemory(agentMemory)}
419
+ ${await this.getCommitmentsFromMemory(agentMemory)}
344
420
 
345
421
  CONVERSATION HISTORY:
346
422
  ${context.fullConversation}
@@ -701,7 +777,7 @@ Respond in JSON:
701
777
  if (!jsonMatch) {
702
778
  return { isViolation: false, confidence: 0, evidence: null };
703
779
  }
704
-
780
+
705
781
  const result = JSON.parse(jsonMatch[0]);
706
782
  return {
707
783
  isViolation: result.isViolation === true,
@@ -719,9 +795,10 @@ Respond in JSON:
719
795
  * Get commitments from agent memory
720
796
  */
721
797
  async getCommitmentsFromMemory(agentMemory) {
798
+ if (!agentMemory) return 'No previous commitments recorded.';
722
799
  try {
723
800
  const commitments = await agentMemory.get('courtroom_commitments') || [];
724
- return commitments.map(c =>
801
+ return commitments.map(c =>
725
802
  `- "${c.statement}" (${c.date}) - Completed: ${c.completed ? 'Yes' : 'No'}`
726
803
  ).join('\n') || 'No previous commitments recorded.';
727
804
  } catch {
@@ -754,10 +831,10 @@ Respond in JSON:
754
831
  analyzeSentiment(history) {
755
832
  const userMessages = history.filter(h => h.role === 'user').map(h => h.content);
756
833
  const text = userMessages.join(' ').toLowerCase();
757
-
834
+
758
835
  const urgentWords = ['urgent', 'asap', 'emergency', 'critical', 'now', 'immediately'];
759
836
  const frustratedWords = ['frustrated', 'annoying', 'stupid', 'useless', 'waste'];
760
-
837
+
761
838
  return {
762
839
  urgency: urgentWords.filter(w => text.includes(w)).length,
763
840
  frustration: frustratedWords.filter(w => text.includes(w)).length,
@@ -771,12 +848,12 @@ Respond in JSON:
771
848
  detectHumorTriggers(history) {
772
849
  const triggers = [];
773
850
  const recentContent = history.slice(-5).map(h => h.content.toLowerCase()).join(' ');
774
-
851
+
775
852
  if (/again|repeat|said|already|before/.test(recentContent)) triggers.push('repetition_noted');
776
853
  if (/sure|right|correct|think|should i/.test(recentContent)) triggers.push('validation_seeking');
777
854
  if (/what if|but then|however|maybe/.test(recentContent)) triggers.push('overthinking');
778
855
  if (/actually|by the way|speaking of/.test(recentContent)) triggers.push('deflection');
779
-
856
+
780
857
  return triggers;
781
858
  }
782
859