← Back to Files
ToolCallLogService.cs
using System.Collections.Concurrent;
using System.Text.Json;
using KaiChat.Models;
namespace KaiChat.Services;
public class ToolCallLogService : IToolCallLogService
{
private readonly ConcurrentQueue<ToolCallLog> _entries = new();
private const int MaxEntries = 500;
private readonly string _filePath;
private readonly object _saveLock = new();
public ToolCallLogService(string filePath)
{
_filePath = filePath;
LoadFromDisk();
}
public string LogFilePath => _filePath;
public void Add(ToolCallLog entry)
{
_entries.Enqueue(entry);
while (_entries.Count > MaxEntries)
_entries.TryDequeue(out _);
SaveToDisk();
}
public List<ToolCallLog> GetRecent(int count = 100)
{
return _entries.OrderByDescending(l => l.Timestamp).Take(count).ToList();
}
public void Clear()
{
while (_entries.TryDequeue(out _)) { }
SaveToDisk();
}
public int GetTotalCount()
{
return _entries.Count;
}
private void LoadFromDisk()
{
try
{
if (!File.Exists(_filePath)) return;
var json = File.ReadAllText(_filePath);
var logs = JsonSerializer.Deserialize<List<ToolCallLog>>(json);
if (logs == null) return;
// File may be in any order (old format was newest-first). Enqueue oldest-first
// so the queue order is consistent and GetRecent's Reverse() gives newest-first.
logs = logs.OrderBy(l => l.Timestamp).ToList();
foreach (var log in logs)
{
_entries.Enqueue(log);
if (_entries.Count > MaxEntries)
_entries.TryDequeue(out _);
}
}
catch
{
// Corrupted file — ignore, start fresh
}
}
private void SaveToDisk()
{
try
{
var dir = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
// Store oldest-first — matches the natural queue order
var logs = _entries.Take(MaxEntries).ToList();
var json = JsonSerializer.Serialize(logs, new JsonSerializerOptions { WriteIndented = true });
lock (_saveLock)
{
File.WriteAllText(_filePath, json);
}
}
catch
{
// Non-critical persistence failure — don't crash
}
}
}