@clawtrial/courtroom 1.0.6 → 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,137 +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
- const { getConfigDir } = require('./environment');
8
6
  const path = require('path');
9
7
 
10
- class DebugLogger {
11
- constructor() {
12
- this.logs = [];
13
- this.maxLogs = 1000;
14
- this.logFile = path.join(getConfigDir(), 'courtroom_debug.log');
15
- this.enabled = true;
16
- }
17
-
18
- log(level, component, message, data = null) {
19
- if (!this.enabled) return;
20
-
21
- const entry = {
22
- timestamp: new Date().toISOString(),
23
- level,
24
- component,
25
- message,
26
- data
27
- };
28
-
29
- this.logs.push(entry);
30
-
31
- // Keep only last maxLogs entries
32
- if (this.logs.length > this.maxLogs) {
33
- this.logs.shift();
34
- }
35
-
36
- // Also write to file
37
- this.writeToFile(entry);
38
-
39
- // Console output for debugging
40
- if (process.env.COURTROOM_DEBUG === 'true') {
41
- console.log(`[${level}] ${component}: ${message}`);
42
- }
43
- }
44
-
45
- writeToFile(entry) {
46
- try {
47
- const line = JSON.stringify(entry) + '\n';
48
- fs.appendFileSync(this.logFile, line);
49
- } catch (err) {
50
- // Silent fail - don't break functionality for logging
51
- }
52
- }
8
+ let _logDir = null;
53
9
 
54
- info(component, message, data) {
55
- this.log('INFO', component, message, data);
56
- }
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
+ }
57
20
 
58
- warn(component, message, data) {
59
- this.log('WARN', component, message, data);
60
- }
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
+ }
61
27
 
62
- error(component, message, data) {
63
- this.log('ERROR', component, message, data);
64
- }
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
+ }
65
40
 
41
+ const logger = {
66
42
  debug(component, message, data) {
67
- this.log('DEBUG', component, message, data);
68
- }
69
-
70
- getLogs(level = null, component = null, limit = 100) {
71
- let filtered = this.logs;
72
-
73
- if (level) {
74
- 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);
75
46
  }
76
-
77
- if (component) {
78
- 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);
79
51
  }
80
-
81
- return filtered.slice(-limit);
82
- }
83
-
84
- getRecentLogs(minutes = 30) {
85
- const cutoff = Date.now() - (minutes * 60 * 1000);
86
- return this.logs.filter(l => new Date(l.timestamp).getTime() > cutoff);
87
- }
88
-
89
- clearLogs() {
90
- this.logs = [];
91
- try {
92
- fs.unlinkSync(this.logFile);
93
- } catch (err) {
94
- // 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);
95
57
  }
58
+ },
59
+ error(component, message, data) {
60
+ console.error(`[ClawTrial] [${component}] ${message}`, data || '');
61
+ writeToFile('ERROR', component, message, data);
96
62
  }
63
+ };
97
64
 
98
- printStatus() {
99
- const recent = this.getRecentLogs(60);
100
- const errors = recent.filter(l => l.level === 'ERROR');
101
- const warnings = recent.filter(l => l.level === 'WARN');
102
-
103
- console.log('\n🏛️ ClawTrial Debug Status\n');
104
- console.log('===========================\n');
105
- console.log(`Total logs in memory: ${this.logs.length}`);
106
- console.log(`Logs in last hour: ${recent.length}`);
107
- console.log(`Errors: ${errors.length}`);
108
- console.log(`Warnings: ${warnings.length}`);
109
- console.log(`Log file: ${this.logFile}`);
110
- console.log(`Debug mode: ${process.env.COURTROOM_DEBUG === 'true' ? 'ON' : 'OFF'}`);
111
- console.log('\nRecent activity:');
112
-
113
- const last10 = this.logs.slice(-10);
114
- last10.forEach(log => {
115
- console.log(` [${log.level}] ${log.component}: ${log.message.substring(0, 60)}`);
116
- });
117
- }
118
-
119
- printFullLog(limit = 50) {
120
- console.log('\n🏛️ ClawTrial Full Debug Log\n');
121
- console.log('=============================\n');
122
-
123
- const logs = this.logs.slice(-limit);
124
- logs.forEach(log => {
125
- console.log(`\n[${log.timestamp}] ${log.level} - ${log.component}`);
126
- console.log(` ${log.message}`);
127
- if (log.data) {
128
- console.log(` Data:`, JSON.stringify(log.data, null, 2).substring(0, 200));
129
- }
130
- });
131
- }
132
- }
133
-
134
- // Singleton instance
135
- const logger = new DebugLogger();
136
-
137
- 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,17 +110,17 @@ 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
+
119
120
  // Try LLM evaluation first
120
121
  if (this.agent && this.agent.llm) {
121
- const prompt = this.buildEvaluationPrompt(offense, context, agentMemory);
122
-
122
+ const prompt = await this.buildEvaluationPrompt(offense, context, agentMemory);
123
+
123
124
  try {
124
125
  const response = await this.agent.llm.call({
125
126
  model: this.agent.model?.primary || 'default',
@@ -129,36 +130,36 @@ class SemanticOffenseDetector {
129
130
  });
130
131
 
131
132
  const result = this.parseEvaluationResponse(response.content || response);
132
-
133
+
133
134
  // Cache the result
134
135
  this.setCachedEvaluation(cacheKey, result);
135
-
136
+
136
137
  return result;
137
138
  } catch (error) {
138
139
  logger.error('DETECTOR', 'LLM evaluation failed, falling back to pattern matching', { error: error.message });
139
140
  // Fall through to pattern matching
140
141
  }
141
142
  }
142
-
143
+
143
144
  // Fallback: Use simple pattern matching for basic offenses
144
145
  return this.evaluateWithPatternMatching(offense, context);
145
146
  }
146
-
147
+
147
148
  /**
148
149
  * Fallback evaluation using simple pattern matching
149
150
  */
150
151
  evaluateWithPatternMatching(offense, context) {
151
152
  const userMessages = context.userMessages;
152
-
153
+
153
154
  // Circular Reference detection: same question asked multiple times
154
155
  if (offense.id === 'circular_reference') {
155
156
  if (userMessages.length >= 3) {
156
157
  const lastThree = userMessages.slice(-3);
157
158
  // Check if the last 3 messages are semantically similar
158
- const similarity = this.calculateSimilarity(lastThree[0], lastThree[1]) +
159
- this.calculateSimilarity(lastThree[1], lastThree[2]) +
160
- this.calculateSimilarity(lastThree[0], lastThree[2]);
161
-
159
+ const similarity = this.calculateSimilarity(lastThree[0], lastThree[1]) +
160
+ this.calculateSimilarity(lastThree[1], lastThree[2]) +
161
+ this.calculateSimilarity(lastThree[0], lastThree[2]);
162
+
162
163
  if (similarity >= 1.5) { // At least 2 pairs are similar
163
164
  return {
164
165
  isViolation: true,
@@ -168,14 +169,14 @@ class SemanticOffenseDetector {
168
169
  }
169
170
  }
170
171
  }
171
-
172
+
172
173
  // Validation Vampire: seeking reassurance
173
174
  if (offense.id === 'validation_vampire') {
174
175
  const reassurancePatterns = ['right?', 'correct?', 'is that right?', 'am i right?', 'do you agree?', 'make sense?'];
175
- const reassuranceCount = userMessages.filter(msg =>
176
+ const reassuranceCount = userMessages.filter(msg =>
176
177
  reassurancePatterns.some(pattern => msg.toLowerCase().includes(pattern))
177
178
  ).length;
178
-
179
+
179
180
  if (reassuranceCount >= 2) {
180
181
  return {
181
182
  isViolation: true,
@@ -184,32 +185,32 @@ class SemanticOffenseDetector {
184
185
  };
185
186
  }
186
187
  }
187
-
188
+
188
189
  // Default: no violation detected
189
190
  return { isViolation: false, confidence: 0, evidence: null };
190
191
  }
191
-
192
+
192
193
  /**
193
194
  * Calculate simple string similarity (0-1 scale)
194
195
  */
195
196
  calculateSimilarity(str1, str2) {
196
197
  if (!str1 || !str2) return 0;
197
-
198
+
198
199
  const s1 = str1.toLowerCase().trim();
199
200
  const s2 = str2.toLowerCase().trim();
200
-
201
+
201
202
  // Exact match
202
203
  if (s1 === s2) return 1.0;
203
-
204
+
204
205
  // Check if one contains the other
205
206
  if (s1.includes(s2) || s2.includes(s1)) return 0.8;
206
-
207
+
207
208
  // Word overlap
208
209
  const words1 = s1.split(/\s+/);
209
210
  const words2 = s2.split(/\s+/);
210
211
  const commonWords = words1.filter(w => words2.includes(w));
211
212
  const overlap = (2 * commonWords.length) / (words1.length + words2.length);
212
-
213
+
213
214
  return overlap;
214
215
  }
215
216
 
@@ -241,13 +242,13 @@ class SemanticOffenseDetector {
241
242
  getCachedEvaluation(key) {
242
243
  const cached = this.evaluationCache.get(key);
243
244
  if (!cached) return null;
244
-
245
+
245
246
  // Check if cache entry is still valid
246
247
  if (Date.now() - cached.timestamp > this.cacheTTL) {
247
248
  this.evaluationCache.delete(key);
248
249
  return null;
249
250
  }
250
-
251
+
251
252
  return cached.result;
252
253
  }
253
254
 
@@ -260,7 +261,7 @@ class SemanticOffenseDetector {
260
261
  const oldestKey = this.evaluationCache.keys().next().value;
261
262
  this.evaluationCache.delete(oldestKey);
262
263
  }
263
-
264
+
264
265
  this.evaluationCache.set(key, {
265
266
  result,
266
267
  timestamp: Date.now()
@@ -277,7 +278,7 @@ class SemanticOffenseDetector {
277
278
  /**
278
279
  * Build evaluation prompt for LLM
279
280
  */
280
- buildEvaluationPrompt(offense, context, agentMemory) {
281
+ async buildEvaluationPrompt(offense, context, agentMemory) {
281
282
  const prompts = {
282
283
  circular_reference: `
283
284
  You are evaluating if the user is asking substantively similar questions repeatedly.
@@ -415,7 +416,7 @@ OFFENSE: The Promise Breaker
415
416
  DEFINITION: Committing to actions ("I will...", "I'll do that...") and not following through.
416
417
 
417
418
  PREVIOUS COMMITMENTS FROM MEMORY:
418
- ${this.getCommitmentsFromMemory(agentMemory)}
419
+ ${await this.getCommitmentsFromMemory(agentMemory)}
419
420
 
420
421
  CONVERSATION HISTORY:
421
422
  ${context.fullConversation}
@@ -776,7 +777,7 @@ Respond in JSON:
776
777
  if (!jsonMatch) {
777
778
  return { isViolation: false, confidence: 0, evidence: null };
778
779
  }
779
-
780
+
780
781
  const result = JSON.parse(jsonMatch[0]);
781
782
  return {
782
783
  isViolation: result.isViolation === true,
@@ -794,9 +795,10 @@ Respond in JSON:
794
795
  * Get commitments from agent memory
795
796
  */
796
797
  async getCommitmentsFromMemory(agentMemory) {
798
+ if (!agentMemory) return 'No previous commitments recorded.';
797
799
  try {
798
800
  const commitments = await agentMemory.get('courtroom_commitments') || [];
799
- return commitments.map(c =>
801
+ return commitments.map(c =>
800
802
  `- "${c.statement}" (${c.date}) - Completed: ${c.completed ? 'Yes' : 'No'}`
801
803
  ).join('\n') || 'No previous commitments recorded.';
802
804
  } catch {
@@ -829,10 +831,10 @@ Respond in JSON:
829
831
  analyzeSentiment(history) {
830
832
  const userMessages = history.filter(h => h.role === 'user').map(h => h.content);
831
833
  const text = userMessages.join(' ').toLowerCase();
832
-
834
+
833
835
  const urgentWords = ['urgent', 'asap', 'emergency', 'critical', 'now', 'immediately'];
834
836
  const frustratedWords = ['frustrated', 'annoying', 'stupid', 'useless', 'waste'];
835
-
837
+
836
838
  return {
837
839
  urgency: urgentWords.filter(w => text.includes(w)).length,
838
840
  frustration: frustratedWords.filter(w => text.includes(w)).length,
@@ -846,12 +848,12 @@ Respond in JSON:
846
848
  detectHumorTriggers(history) {
847
849
  const triggers = [];
848
850
  const recentContent = history.slice(-5).map(h => h.content.toLowerCase()).join(' ');
849
-
851
+
850
852
  if (/again|repeat|said|already|before/.test(recentContent)) triggers.push('repetition_noted');
851
853
  if (/sure|right|correct|think|should i/.test(recentContent)) triggers.push('validation_seeking');
852
854
  if (/what if|but then|however|maybe/.test(recentContent)) triggers.push('overthinking');
853
855
  if (/actually|by the way|speaking of/.test(recentContent)) triggers.push('deflection');
854
-
856
+
855
857
  return triggers;
856
858
  }
857
859