@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/README.md +70 -94
- package/package.json +21 -26
- package/scripts/postinstall.js +28 -79
- package/skills/courtroom/SKILL.md +49 -0
- package/src/api.js +55 -21
- package/src/crypto.js +13 -11
- package/src/debug.js +49 -120
- package/src/detector.js +112 -35
- package/src/hearing.js +203 -384
- package/src/plugin.js +435 -0
- package/src/punishment.js +105 -249
- package/src/storage.js +68 -0
- package/SECURITY.md +0 -124
- package/SKILL.md +0 -50
- package/TECHNICAL_OVERVIEW.md +0 -278
- package/_meta.json +0 -6
- package/clawdbot.plugin.json +0 -32
- package/scripts/clawtrial.js +0 -578
- package/scripts/cli.js +0 -184
- package/skill.yaml +0 -64
- package/src/autostart.js +0 -175
- package/src/config.js +0 -209
- package/src/consent.js +0 -215
- package/src/core.js +0 -208
- package/src/daemon.js +0 -151
- package/src/detector-v1.js +0 -572
- package/src/environment.js +0 -267
- package/src/hook.js +0 -265
- package/src/index.js +0 -286
- package/src/monitor.js +0 -193
- package/src/skill.js +0 -355
- package/src/standalone.js +0 -247
package/src/debug.js
CHANGED
|
@@ -1,136 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Debug Logger
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
47
|
+
},
|
|
48
|
+
info(component, message, data) {
|
|
49
|
+
if (CURRENT_LEVEL <= LOG_LEVELS.INFO) {
|
|
50
|
+
writeToFile('INFO', component, message, data);
|
|
78
51
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|