From 95105858c0eca3a323885f8c9f4fae02274728f6 Mon Sep 17 00:00:00 2001 From: Tobias Garcia Date: Mon, 25 May 2026 14:47:21 +0900 Subject: [PATCH 1/3] fix: display user-invoked skills in Skills icon in Dashboard --- app/internal_action_interface.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/internal_action_interface.py b/app/internal_action_interface.py index 1ba597ad..9e67d674 100644 --- a/app/internal_action_interface.py +++ b/app/internal_action_interface.py @@ -480,6 +480,16 @@ async def do_create_task( # Merge: skill-recommended + LLM-selected (deduplicated) all_action_sets = list(dict.fromkeys(skill_action_sets + llm_action_sets)) logger.info(f"[TASK] Pre-selected skills (via command): {selected_skills}") + try: + from app.ui_layer.metrics.collector import MetricsCollector + collector = MetricsCollector.get_instance() + if collector: + logger.info(f"[TASK] Pre-selected skills collector initialized") + for skill_name in selected_skills: + collector.record_skill_invocation(skill_name) + except Exception: + pass + else: # Select skills and action sets in a single LLM call (optimized) # Skills are selected first, then action sets with knowledge of skill recommendations @@ -901,7 +911,6 @@ async def _select_skills_and_action_sets_via_llm( collector.record_skill_invocation(skill_name) except Exception: pass # Don't fail skill selection if metrics recording fails - return valid_skills, valid_sets except json.JSONDecodeError as e: From 1fc701402e0b7d43b4fac023d635d120eeac2cd4 Mon Sep 17 00:00:00 2001 From: Tobias Garcia Date: Mon, 25 May 2026 15:31:54 +0900 Subject: [PATCH 2/3] fix: skill information (invocations, top skills) persist across sessions --- app/ui_layer/metrics/collector.py | 26 ++++++++++++++++++++++++++ app/usage/__init__.py | 8 ++++++++ 2 files changed, 34 insertions(+) diff --git a/app/ui_layer/metrics/collector.py b/app/ui_layer/metrics/collector.py index 526500f6..97a25c7d 100644 --- a/app/ui_layer/metrics/collector.py +++ b/app/ui_layer/metrics/collector.py @@ -471,6 +471,7 @@ def __init__(self, agent: Optional["AgentBase"] = None) -> None: # Storage references for historical data self._usage_storage = None self._task_storage = None + self._skill_storage = None self._init_storage() def _init_storage(self) -> None: @@ -484,6 +485,26 @@ def _init_storage(self) -> None: except Exception: # Storage may not be available in all contexts pass + try: + from app.usage.skill_storage import get_skill_storage + + self._skill_storage = get_skill_storage() + except Exception: + pass + self._load_skill_metrics() + + def _load_skill_metrics(self) -> None: + """Restore skill invocation counts from persistent storage on startup.""" + if not self._skill_storage: + return + try: + totals = self._skill_storage.get_skill_totals() + with self._lock: + for skill_name, count in totals.items(): + self._skill_usage[skill_name] = count + self._skill_total_invocations = sum(totals.values()) + except Exception: + pass def _get_period_bounds( self, period: TimePeriod @@ -654,6 +675,11 @@ def record_skill_invocation(self, skill_name: str) -> None: with self._lock: self._skill_usage[skill_name] += 1 self._skill_total_invocations += 1 + if self._skill_storage: + try: + self._skill_storage.insert_invocation(skill_name) + except Exception: + pass def get_top_skills(self, limit: int = 3) -> List[Tuple[str, int]]: """Get top N most used skills.""" diff --git a/app/usage/__init__.py b/app/usage/__init__.py index f47774d9..2f10d810 100644 --- a/app/usage/__init__.py +++ b/app/usage/__init__.py @@ -36,6 +36,11 @@ get_action_storage, ) +from app.usage.skill_storage import ( + SkillStorage, + get_skill_storage, +) + __all__ = [ # Storage "UsageEvent", @@ -57,4 +62,7 @@ "StoredActionItem", "ActionStorage", "get_action_storage", + # Skill Storage + "SkillStorage", + "get_skill_storage", ] From e2f3e390c76d08a784c42844e8c9b2037d9ba218 Mon Sep 17 00:00:00 2001 From: Tobias Garcia Date: Mon, 25 May 2026 15:33:51 +0900 Subject: [PATCH 3/3] fix: skill information (invocations, top skills) persist across sessions + (upload missing file) --- app/usage/skill_storage.py | 134 +++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/usage/skill_storage.py diff --git a/app/usage/skill_storage.py b/app/usage/skill_storage.py new file mode 100644 index 00000000..daa79a00 --- /dev/null +++ b/app/usage/skill_storage.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +app.usage.skill_storage + +SQLite-based storage for skill invocation events. +Provides local persistence for skill usage history across restarts. +""" + +from __future__ import annotations + +import logging +import sqlite3 +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional + +try: + from app.logger import logger +except Exception: + logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + +class SkillStorage: + """ + SQLite-based storage for skill invocation events. + + Provides local persistence for skill usage history. + Events are stored in a SQLite database in app/data/.usage/skills.db. + """ + + def __init__(self, db_path: Optional[str] = None): + if db_path is None: + from app.config import APP_DATA_PATH + + usage_dir = Path(APP_DATA_PATH) / ".usage" + usage_dir.mkdir(parents=True, exist_ok=True) + db_path = str(usage_dir / "skills.db") + + self._db_path = db_path + self._init_db() + logger.info(f"[SkillStorage] Initialized at {self._db_path}") + + def _init_db(self) -> None: + """Initialize the database schema.""" + with sqlite3.connect(self._db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS skill_invocations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, + skill_name TEXT NOT NULL + ) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_skill_timestamp + ON skill_invocations(timestamp) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_skill_name + ON skill_invocations(skill_name) + """) + conn.commit() + + def insert_invocation(self, skill_name: str) -> int: + """ + Record a single skill invocation. + + Args: + skill_name: Name of the skill that was invoked. + + Returns: + The row ID of the inserted record. + """ + with sqlite3.connect(self._db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO skill_invocations (timestamp, skill_name) VALUES (?, ?)", + (datetime.now().isoformat(), skill_name), + ) + conn.commit() + return cursor.lastrowid + + def get_skill_totals(self) -> Dict[str, int]: + """ + Get all-time invocation counts grouped by skill name. + + Returns: + Dict mapping skill_name -> total invocation count. + """ + with sqlite3.connect(self._db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT skill_name, COUNT(*) FROM skill_invocations GROUP BY skill_name + """) + return {row[0]: row[1] for row in cursor.fetchall()} + + def get_stats(self) -> Dict: + """Get storage statistics.""" + with sqlite3.connect(self._db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM skill_invocations") + total = cursor.fetchone()[0] + cursor.execute("SELECT MIN(timestamp), MAX(timestamp) FROM skill_invocations") + row = cursor.fetchone() + return { + "db_path": self._db_path, + "total_invocations": total, + "earliest": row[0] if row[0] else None, + "latest": row[1] if row[1] else None, + } + + def clear_invocations(self) -> int: + """Clear all invocation records. Returns number deleted.""" + with sqlite3.connect(self._db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM skill_invocations") + count = cursor.fetchone()[0] + cursor.execute("DELETE FROM skill_invocations") + conn.commit() + logger.info(f"[SkillStorage] Cleared {count} invocation records") + return count + + +# Global storage instance +_skill_storage: Optional[SkillStorage] = None + + +def get_skill_storage() -> SkillStorage: + """Get the global skill storage instance.""" + global _skill_storage + if _skill_storage is None: + _skill_storage = SkillStorage() + return _skill_storage