/ utils / suggestions / skillUsageTracking.ts
skillUsageTracking.ts
 1  import { getGlobalConfig, saveGlobalConfig } from '../config.js'
 2  
 3  const SKILL_USAGE_DEBOUNCE_MS = 60_000
 4  
 5  // Process-lifetime debounce cache — avoids lock + read + parse on debounced
 6  // calls. Same pattern as lastConfigStatTime / globalConfigWriteCount in config.ts.
 7  const lastWriteBySkill = new Map<string, number>()
 8  
 9  /**
10   * Records a skill usage for ranking purposes.
11   * Updates both usage count and last used timestamp.
12   */
13  export function recordSkillUsage(skillName: string): void {
14    const now = Date.now()
15    const lastWrite = lastWriteBySkill.get(skillName)
16    // The ranking algorithm uses a 7-day half-life, so sub-minute granularity
17    // is irrelevant. Bail out before saveGlobalConfig to avoid lock + file I/O.
18    if (lastWrite !== undefined && now - lastWrite < SKILL_USAGE_DEBOUNCE_MS) {
19      return
20    }
21    lastWriteBySkill.set(skillName, now)
22    saveGlobalConfig(current => {
23      const existing = current.skillUsage?.[skillName]
24      return {
25        ...current,
26        skillUsage: {
27          ...current.skillUsage,
28          [skillName]: {
29            usageCount: (existing?.usageCount ?? 0) + 1,
30            lastUsedAt: now,
31          },
32        },
33      }
34    })
35  }
36  
37  /**
38   * Calculates a usage score for a skill based on frequency and recency.
39   * Higher scores indicate more frequently and recently used skills.
40   *
41   * The score uses exponential decay with a half-life of 7 days,
42   * meaning usage from 7 days ago is worth half as much as usage today.
43   */
44  export function getSkillUsageScore(skillName: string): number {
45    const config = getGlobalConfig()
46    const usage = config.skillUsage?.[skillName]
47    if (!usage) return 0
48  
49    // Recency decay: halve score every 7 days
50    const daysSinceUse = (Date.now() - usage.lastUsedAt) / (1000 * 60 * 60 * 24)
51    const recencyFactor = Math.pow(0.5, daysSinceUse / 7)
52  
53    // Minimum recency factor of 0.1 to avoid completely dropping old but heavily used skills
54    return usage.usageCount * Math.max(recencyFactor, 0.1)
55  }