clementine-agent 1.1.7 → 1.1.8

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.
@@ -271,7 +271,7 @@ export declare class PersonalAssistant {
271
271
  * so follow-up conversation has context.
272
272
  */
273
273
  injectContext(sessionKey: string, userText: string, assistantText: string): void;
274
- getRecentActivity(sinceIso: string): Array<{
274
+ getRecentActivity(sinceIso: string, maxEntries?: number): Array<{
275
275
  sessionKey: string;
276
276
  role: string;
277
277
  content: string;
@@ -4828,11 +4828,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
4828
4828
  }
4829
4829
  }
4830
4830
  }
4831
- getRecentActivity(sinceIso) {
4831
+ getRecentActivity(sinceIso, maxEntries) {
4832
4832
  if (!this.memoryStore)
4833
4833
  return [];
4834
4834
  try {
4835
- return this.memoryStore.getRecentActivity(sinceIso);
4835
+ return this.memoryStore.getRecentActivity(sinceIso, maxEntries);
4836
4836
  }
4837
4837
  catch {
4838
4838
  return [];
@@ -47,13 +47,25 @@ export declare function maybeIncreaseCooldown(state: InsightState): void;
47
47
  * Returns structured event summaries that can be passed to an LLM for urgency rating.
48
48
  */
49
49
  export declare function gatherInsightSignals(gateway: {
50
- getRecentActivity: (since: string) => Array<{
50
+ getRecentActivity: (since: string, maxEntries?: number) => Array<{
51
51
  sessionKey: string;
52
52
  role: string;
53
53
  content: string;
54
54
  createdAt: string;
55
55
  }>;
56
56
  }): string[];
57
+ export declare function detectFrustrationSignals(activity: Array<{
58
+ sessionKey: string;
59
+ role: string;
60
+ content: string;
61
+ createdAt: string;
62
+ }>): string[];
63
+ export declare function detectRepeatedTopics(activity: Array<{
64
+ sessionKey: string;
65
+ role: string;
66
+ content: string;
67
+ createdAt: string;
68
+ }>): string[];
57
69
  /**
58
70
  * Build a prompt for urgency rating (to be sent to a lightweight LLM).
59
71
  * Returns null if there are no signals worth evaluating.
@@ -189,7 +189,27 @@ export function gatherInsightSignals(gateway) {
189
189
  catch (err) {
190
190
  logger.debug({ err }, 'Failed to pull broken-jobs signals');
191
191
  }
192
- // 6. Claim tracker failed claims in the last N hours erode trust.
192
+ // 6. Conversational signals derived from recent transcripts.
193
+ // Surfaces patterns IN the conversation itself, not just system events:
194
+ // user frustration markers, repeating topics, etc. These are early
195
+ // warning signs that the agent's responses may be off-track.
196
+ try {
197
+ const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
198
+ const since7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
199
+ // 24h frustration scan — 50 entries plenty to count corrections in a day.
200
+ const recent = gateway.getRecentActivity(since24h, 50);
201
+ for (const s of detectFrustrationSignals(recent))
202
+ signals.push(s);
203
+ // 7d repeat-topic scan — pull more entries since topics span sessions.
204
+ // Cap at 200 to keep keyword extraction cheap.
205
+ const week = gateway.getRecentActivity(since7d, 200);
206
+ for (const s of detectRepeatedTopics(week))
207
+ signals.push(s);
208
+ }
209
+ catch (err) {
210
+ logger.debug({ err }, 'Failed to pull conversational signals');
211
+ }
212
+ // 7. Claim tracker — failed claims in the last N hours erode trust.
193
213
  // Surface them so the owner sees "Clementine said she'd do X; she
194
214
  // didn't" instead of silently swallowing the miss.
195
215
  try {
@@ -214,6 +234,95 @@ export function gatherInsightSignals(gateway) {
214
234
  }
215
235
  return signals;
216
236
  }
237
+ // ── Conversational signal detectors ─────────────────────────────────
238
+ //
239
+ // Pure functions over recent transcript activity. Exported so the insight
240
+ // dashboard / debug commands can run them independently of the full
241
+ // gatherInsightSignals path.
242
+ /**
243
+ * Markers that suggest the user is correcting or frustrated with the
244
+ * agent's last response. Tuned to start-of-message tokens since
245
+ * mid-message "no" or "actually" is often just normal narrative.
246
+ */
247
+ const CORRECTION_PATTERNS = [
248
+ /^(no|nope|not\b)/i,
249
+ /^(actually|wait)\b/i,
250
+ /^(that['’]?s| that is) (wrong|not|incorrect|backwards|opposite)/i,
251
+ /^I (meant|said|wanted|asked)\b/i,
252
+ /^you (didn['’]?t|misunderstood|got it wrong|missed)/i,
253
+ /^(stop|cancel|undo|nevermind|never mind)\b/i,
254
+ ];
255
+ export function detectFrustrationSignals(activity) {
256
+ const signals = [];
257
+ let count = 0;
258
+ const sessionsAffected = new Set();
259
+ for (const entry of activity) {
260
+ if (entry.role !== 'user')
261
+ continue;
262
+ const trimmed = entry.content.trim();
263
+ for (const re of CORRECTION_PATTERNS) {
264
+ if (re.test(trimmed)) {
265
+ count++;
266
+ sessionsAffected.add(entry.sessionKey);
267
+ break;
268
+ }
269
+ }
270
+ }
271
+ if (count >= 3) {
272
+ signals.push(`Conversation friction: ${count} user correction(s) across ${sessionsAffected.size} session(s) in the last 24h — recent agent responses may be off-track`);
273
+ }
274
+ return signals;
275
+ }
276
+ /**
277
+ * Words too generic to count as a topic — would otherwise dominate the
278
+ * "recurring topic" signal with noise like "thanks", "okay", "please".
279
+ */
280
+ const TOPIC_STOPWORDS = new Set([
281
+ 'about', 'after', 'again', 'against', 'because', 'before', 'being', 'between',
282
+ 'could', 'doing', 'don’t', 'down', 'during', 'each', 'from', 'further',
283
+ 'going', 'gonna', 'have', 'having', 'here', 'into', 'just', 'know', 'like',
284
+ 'maybe', 'might', 'more', 'most', 'much', 'need', 'okay', 'only', 'other',
285
+ 'over', 'please', 'really', 'said', 'same', 'some', 'still', 'such', 'than',
286
+ 'that', 'them', 'then', 'there', 'these', 'they', 'thing', 'think', 'this',
287
+ 'those', 'through', 'thanks', 'time', 'told', 'under', 'until', 'using', 'very',
288
+ 'want', 'wanted', 'wants', 'were', 'what', 'when', 'where', 'which', 'while',
289
+ 'will', 'with', 'would', 'your', 'yours', 'yeah', 'yes',
290
+ 'tonight', 'today', 'tomorrow', 'morning', 'evening', 'session', 'work',
291
+ 'doing', 'made', 'make', 'making', 'sure', 'right', 'wrong', 'good', 'bad',
292
+ 'much', 'many', 'lots',
293
+ ]);
294
+ export function detectRepeatedTopics(activity) {
295
+ // Build a (keyword → set of session IDs) map. A keyword that shows up in
296
+ // 3+ DISTINCT sessions across the window is "recurring" — could be an
297
+ // unresolved thread, a project the user is grinding on, or a question
298
+ // they've asked multiple ways.
299
+ const sessionsForKeyword = new Map();
300
+ for (const entry of activity) {
301
+ if (entry.role !== 'user')
302
+ continue;
303
+ const text = entry.content.toLowerCase();
304
+ // Word extraction: 5+ chars, alpha-only (no numbers/punctuation).
305
+ const matches = text.match(/[a-z][a-z’]{4,15}/g) ?? [];
306
+ const seenInThisMessage = new Set();
307
+ for (const w of matches) {
308
+ if (TOPIC_STOPWORDS.has(w))
309
+ continue;
310
+ if (seenInThisMessage.has(w))
311
+ continue; // dedupe within a single message
312
+ seenInThisMessage.add(w);
313
+ if (!sessionsForKeyword.has(w))
314
+ sessionsForKeyword.set(w, new Set());
315
+ sessionsForKeyword.get(w).add(entry.sessionKey);
316
+ }
317
+ }
318
+ // Rank by session-spread; surface the top 2 to avoid flooding insight
319
+ // notifications with too many topic mentions.
320
+ const ranked = [...sessionsForKeyword.entries()]
321
+ .filter(([, sessions]) => sessions.size >= 3)
322
+ .sort((a, b) => b[1].size - a[1].size)
323
+ .slice(0, 2);
324
+ return ranked.map(([keyword, sessions]) => `Recurring topic "${keyword}" came up across ${sessions.size} sessions this week — possible ongoing thread`);
325
+ }
217
326
  /**
218
327
  * Build a prompt for urgency rating (to be sent to a lightweight LLM).
219
328
  * Returns null if there are no signals worth evaluating.
@@ -170,7 +170,7 @@ export declare class Gateway {
170
170
  * Get recent transcript activity across all sessions.
171
171
  * Used by heartbeat to know what happened since the last check.
172
172
  */
173
- getRecentActivity(sinceIso: string): Array<{
173
+ getRecentActivity(sinceIso: string, maxEntries?: number): Array<{
174
174
  sessionKey: string;
175
175
  role: string;
176
176
  content: string;
@@ -1447,8 +1447,8 @@ export class Gateway {
1447
1447
  * Get recent transcript activity across all sessions.
1448
1448
  * Used by heartbeat to know what happened since the last check.
1449
1449
  */
1450
- getRecentActivity(sinceIso) {
1451
- return this.assistant.getRecentActivity(sinceIso);
1450
+ getRecentActivity(sinceIso, maxEntries) {
1451
+ return this.assistant.getRecentActivity(sinceIso, maxEntries);
1452
1452
  }
1453
1453
  /**
1454
1454
  * Search memory (FTS5) for context relevant to a query.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",