Add API usage tracking and dynamic task reloading

Features:
- Usage tracking system (usage_tracker.py)
  - Tracks input/output tokens per API call
  - Calculates costs with support for cache pricing
  - Stores data in usage_data.json (gitignored)
  - Integrated into llm_interface.py

- Dynamic task scheduler reloading
  - Auto-detects YAML changes every 60s
  - No restart needed for new tasks
  - reload_tasks() method for manual refresh

- Example cost tracking scheduled task
  - Daily API usage report
  - Budget tracking ($5/month target)
  - Disabled by default in scheduled_tasks.yaml

Improvements:
- Fixed tool_use/tool_result pair splitting bug (CRITICAL)
- Added thread safety to agent.chat()
- Fixed N+1 query problem in hybrid search
- Optimized database batch queries
- Added conversation history pruning (50 messages max)

Updated .gitignore:
- Exclude user profiles (memory_workspace/users/*.md)
- Exclude usage data (usage_data.json)
- Exclude vector index (vectors.usearch)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 23:38:44 -07:00
parent ab3a5afd59
commit 8afff96bb5
16 changed files with 1096 additions and 244 deletions

View File

@@ -84,8 +84,20 @@ class AdapterRuntime:
self._postprocessors.append(postprocessor)
def _on_message_received(self, message: InboundMessage) -> None:
"""Handle incoming message from an adapter."""
asyncio.create_task(self._message_queue.put(message))
"""Handle incoming message from an adapter.
This may be called from different event loop contexts (e.g.,
python-telegram-bot's internal loop vs. our main asyncio loop),
so we use loop-safe scheduling instead of create_task().
"""
try:
loop = asyncio.get_running_loop()
loop.call_soon_threadsafe(self._message_queue.put_nowait, message)
except RuntimeError:
# No running loop - should not happen in normal operation
# but handle gracefully
print("[Runtime] Warning: No event loop for message dispatch")
self._message_queue.put_nowait(message)
async def _process_message_queue(self) -> None:
"""Background task to process incoming messages."""

View File

@@ -39,6 +39,7 @@ class SlackAdapter(BaseAdapter):
super().__init__(config)
self.app: Optional[AsyncApp] = None
self.handler: Optional[AsyncSocketModeHandler] = None
self._username_cache: Dict[str, str] = {} # user_id -> username
@property
def platform_name(self) -> str:
@@ -255,7 +256,15 @@ class SlackAdapter(BaseAdapter):
}
async def _get_username(self, user_id: str) -> str:
"""Get username from user ID."""
"""Get username from user ID, with caching to avoid excessive API calls.
Sanitizes the returned username to contain only alphanumeric,
hyphens, and underscores (matching memory_system validation rules).
"""
# Check cache first
if user_id in self._username_cache:
return self._username_cache[user_id]
if not self.app:
return user_id
@@ -263,13 +272,20 @@ class SlackAdapter(BaseAdapter):
result = await self.app.client.users_info(user=user_id)
user = result["user"]
profile = user.get("profile", {})
return (
raw_username = (
profile.get("display_name")
or profile.get("real_name")
or user.get("name")
or user_id
)
# Sanitize: replace spaces/special chars with underscores
sanitized = "".join(
c if c.isalnum() or c in "-_" else "_" for c in raw_username
)
self._username_cache[user_id] = sanitized
return sanitized
except SlackApiError:
self._username_cache[user_id] = user_id
return user_id
@staticmethod

View File

@@ -224,12 +224,22 @@ class TelegramAdapter(BaseAdapter):
else None
)
sent_message = await self.bot.send_message(
chat_id=chat_id,
text=chunk,
parse_mode=parse_mode,
reply_to_message_id=reply_to_id,
)
try:
sent_message = await self.bot.send_message(
chat_id=chat_id,
text=chunk,
parse_mode=parse_mode,
reply_to_message_id=reply_to_id,
)
except TelegramError:
# Markdown parse errors are common with LLM-generated
# text (unbalanced *, _, etc). Fall back to plain text.
sent_message = await self.bot.send_message(
chat_id=chat_id,
text=chunk,
parse_mode=None,
reply_to_message_id=reply_to_id,
)
results.append({
"message_id": sent_message.message_id,